打字稿在编译时减去两个数字

时间:2019-03-04 17:14:20

标签: typescript typescript-generics

原始问题

我需要一种实用程序类型Subtract<A, B>,其中AB是数字。例如:

type Subtract<A extends number, B extends number> = /* implementation */ 
const one: Subtract<2, 1> = 1
const two: Subtract<4, 2> = 2
const error: Subtract<2, 1> = 123 // Error here: 123 is not assignable to type '1'.

Subtract<A, B>的参数始终是数字文字或编译时间常数。我不需要

let foo: Subtract<number, number> // 'foo' may have 'number' type.

已编辑的问题

好吧,我认为以上文字可能是XY问题,所以我想解释为什么我需要减去。我有一个多维数组,其维度为Dims。调用slice方法时,其尺寸会减小。

interface Tensor<Dimns extends number> {
    // Here `I` type is a type of indeces 
    // and its 'length' is how many dimensions 
    // are subtracted from source array.
    slice<I extends Array<[number, number]>>(...indeces: I): Tensor<Dimns - I['length']>
                                               // here I need to subtract ^ 
}

示例:

declare const arr: Tensor<4, number>
arr.slice([0, 1])               // has to be 3d array
arr.slice([0, 1], [0, 2])       // has to be 2d array
arr.slice([0, 1]).slice([0, 2]) // has to be 2d array

您可以看到Dims泛型如何取决于传递给slice()的参数数量。

如果很难进行Subtract<A, B>键入,是否可以减少键入?因此,我可以执行以下操作:

interface Tensor<Dimns extends number> {
    // Here `Decrement<A>` reduces the number of dimensions by 1.
    slice(start: number, end: number): Tensor<Decrement<Dimns>>
}

1 个答案:

答案 0 :(得分:8)

TypeScript不支持编译时算法。但是,可以强制使用数组执行某种类似的操作,但是您必须定义自己的方法算术。我会提前警告您,这绝对可怕。

首先定义一些用于数组操作的基本类型:

type Tail<T> = T extends Array<any> ? ((...x: T) => void) extends ((h: any, ...t: infer I) => void) ? I : [] : unknown;
type Cons<A, T> = T extends Array<any> ? ((a: A, ...t: T) => void) extends ((...i: infer I) => void) ? I : unknown : never;

这些给您一些数组类型的功能,例如Tail<['foo', 'bar']>给您['bar'],而Cons<'foo', ['bar']>给您['foo', 'bar']

现在您可以使用基于数组的数字(而非number)来定义一些算术概念:

type Zero = [];
type Inc<T> = Cons<void, T>;
type Dec<T> = Tail<T>;

因此,数字1在该系统中将表示为[void],2将表示为[void, void],依此类推。我们可以将加减定义为:

type Add<A, B> = { 0: A, 1: Add<Inc<A>, Dec<B>> }[Zero extends B ? 0 : 1];
type Sub<A, B> = { 0: A, 1: Sub<Dec<A>, Dec<B>> }[Zero extends B ? 0 : 1];

如果确定的话,还可以以类似的方式定义乘法和除法运算符。但是就目前而言,这足以用作基本的算术系统。例如:

type One = Inc<Zero>;                    // [void]
type Two = Inc<One>;                     // [void, void]
type Three = Add<One, Two>;              // [void, void, void]
type Four = Sub<Add<Three, Three>, Two>; // [void, void, void, void]

定义一些其他实用程序方法,以从number常量来回转换。

type N<A extends number, T = Zero> = { 0: T, 1: N<A, Inc<T>> }[V<T> extends A ? 0 : 1];
type V<T> = T extends { length: number } ? T['length'] : unknown;

现在您可以像这样使用它们

const one: V<Sub<N<2>, N<1>>> = 1;
const two: V<Sub<N<4>, N<2>>> = 2;
const error: V<Sub<N<2>, N<1>>> = 123; // Type '123' is not assignable to type '1'.

所有这些都是为了显示TypeScript的类型系统有多么强大,以及您可以将其推向多远来执行其并非真正为之设计的工作。它似乎也只能可靠地工作到N<23>左右(可能是由于TypeScript中的递归类型的限制)。但是您实际上应该在生产系统中执行此操作吗?

否!

当然,这种类型滥用很有趣(至少对我而言),但是 far 太复杂了, far 太容易造成了简单的错误,极其困难的调试。我强烈建议您只对常量类型(const one: 1进行硬编码,或者如注释所建议的那样,重新考虑您的设计。


对于更新后的问题,如果可以像上面的Tensor一样轻松地简化Tail类型(考虑到它是一个接口,这是令人怀疑的),则可以执行以下操作:< / p>

type Reduced<T extends Tensor<number>> = T extends Tensor<infer N> ? /* construct Tensor<N-1> from Tensor<N> */ : Tensor<number>;

interface Tensor<Dimns extends number> {
  slice(start: number, end: number): Reduced<Tensor<Dimns>>;
}

但是,由于张量往往只有几个维度,我认为仅在少数情况下编写代码就足够了,而用户最有可能需要担心:

type SliceIndeces<N extends number> = number[] & { length: N };
interface Tensor<Dims extends number> {
  slice(this: Tensor<5>, ...indeces: SliceIndeces<1>): Tensor<4>;
  slice(this: Tensor<5>, ...indeces: SliceIndeces<2>): Tensor<3>;
  slice(this: Tensor<5>, ...indeces: SliceIndeces<3>): Tensor<2>;
  slice(this: Tensor<5>, ...indeces: SliceIndeces<2>): Tensor<1>;
  slice(this: Tensor<4>, ...indeces: SliceIndeces<1>): Tensor<3>;
  slice(this: Tensor<4>, ...indeces: SliceIndeces<2>): Tensor<2>;
  slice(this: Tensor<4>, ...indeces: SliceIndeces<3>): Tensor<1>;
  slice(this: Tensor<3>, ...indeces: SliceIndeces<1>): Tensor<2>;
  slice(this: Tensor<3>, ...indeces: SliceIndeces<2>): Tensor<1>;
  slice(this: Tensor<2>, ...indeces: SliceIndeces<1>): Tensor<1>;
  slice(...indeces:number[]): Tensor<number>;
}

const t5: Tensor<5> = ...
const t3 = t5.slice(0, 5); // inferred type is Tensor<3>

我知道这会导致一些漂亮的“ WET”代码,但是维护该代码的成本仍可能比维护如上所述的自定义算术系统的成本低。

请注意,官方TypeScript声明文件通常使用类似以下的模式(请参见lib.esnext.array.d.ts)。强类型定义仅覆盖最常见的用例。对于任何其他用例,都希望用户在适当的地方提供类型注释/断言。