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 中递归类型的限制)。但是你真的应该在生产系统中这样做吗?
不!
当然,这种类型的滥用有点有趣(至少对我而言),但它远太复杂了,远太容易犯简单的错误极其困难的调试。我强烈建议您只对常量类型 (const one: 1) 进行硬编码,或者按照 cmets 的建议重新考虑您的设计。
对于更新后的问题,如果Tensor 类型可以像上面的Tail 一样轻松地减少(这值得怀疑,因为它是一个接口),你可以这样做:
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)。强类型定义仅涵盖最常见的用例。对于任何其他用例,用户应在适当的情况下提供类型注释/断言。