简体   繁体   English

Typescript在编译时减去两个数字

[英]Typescript subtract two numbers at compile time

Original question 原始问题

I need an utility type Subtract<A, B> where A and B are numbers. 我需要一个实用程序类型Subtract<A, B>其中AB是数字。 For example: 例如:

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'.

Arguments to the Subtract<A, B> are always number literals or compile time constants. Subtract<A, B>的参数总是数字文字或编译时间常数。 I do not need 我不需要

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

Edited question 编辑问题

Ok, I think that above text probably is the XY problem, so I want to explain why I need subtraction. 好吧,我认为上面的文字可能是XY问题,所以我想解释为什么我需要减法。 I have a multidimensional array, which has Dims dimensions. 我有一个多维数组,具有Dims维度。 When a slice method is called, its dimensions are reduced. 调用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 ^ 
}

Examples: 例子:

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

You can see how Dims generic depends on number of arguments passed to slice() . 您可以看到Dims泛型如何依赖于传递给slice()的参数数量。

If it is hard to make Subtract<A, B> type, is it possible to decrement type? 如果很难使Subtract<A, B>类型,是否可以减少类型? So, I can do the following: 所以,我可以做到以下几点:

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

TypeScript doesn't support compile-time arithmetic. TypeScript不支持编译时算术。 However, it can be coerced to do something sort of similar using arrays, but you have to define your own method arithmetic. 但是,可以使用数组强制执行某些类似的操作,但您必须定义自己的方法算法。 I'll warn you up front that it's absolutely terrible. 我会在前面警告你,这绝对是可怕的。

Start with defining a few fundamental types for array manipulation: 首先为数组操作定义一些基本类型:

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;

These give you some power of array types, for example Tail<['foo', 'bar']> gives you ['bar'] and Cons<'foo', ['bar']> gives you ['foo', 'bar'] . 这些给你一些数组类型的力量,例如Tail<['foo', 'bar']>给你['bar']Cons<'foo', ['bar']>给你['foo', 'bar']

Now you can define some arithmetic concepts using array-based numerals (not number ): 现在,您可以使用基于数组的数字(而非number )定义一些算术概念:

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

So the numeral 1 would be represented in this system as [void] , 2 is [void, void] and so on. 因此,数字1将在此系统中表示为[void] ,2为[void, void] ,依此类推。 We can define addition and subtraction as: 我们可以将加法和减法定义为:

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];

If you're determined, you can also define multiplication and division operators in a similar way. 如果您已确定,还可以以类似的方式定义乘法和除法运算符。 But for now, this is good enough to use as a basic system of arithmetic. 但就目前而言,这足以作为一个基本的算术系统使用。 For example: 例如:

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]

Define a few other utility methods to convert back and forth from number constants. 定义一些其他实用程序方法来从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;

And now you can use them like this 现在你可以像这样使用它们了

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'.

All of this was to show how powerful TypeScript's type system is, and just how far you can push it to do things it wasn't really designed for. 所有这一切都是为了展示TypeScript的类型系统有多强大,以及你可以推动它做多少事情而不是真正的设计。 It also only seems to reliably work up to N<23> or so (probably due to limits on recursive types within TypeScript). 它似乎只能可靠地工作到N<23>左右(可能是由于TypeScript中的递归类型的限制)。 But should you actually do this in a production system? 但是你真的应该在生产系统中这样做吗?

No! 没有!

Sure, this sort of type abuse is kind of amusing (at least to me), but it's far too complex and far too easy to make simple mistakes that are extremely difficult debug. 当然,这种类型的虐待是一种有趣的(至少对我来说),但它复杂, 容易使简单的错误,是非常困难的调试。 I highly recommend just hard-coding your constant types ( const one: 1 ) or as the comments suggest, rethinking your design. 我强烈建议您只需对常量类型进行硬编码( const one: 1 ),或者如评论所示,重新考虑您的设计。


For the updated question, if the Tensor type can be easily reduced in the same way Tail does above (which is doubtful given that it's an interface), you could do something like this: 对于更新的问题,如果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>>;
}

However, since tensors tend to only have a few dimensions, I think it's sufficient just to code in a handful of cases the user will most likely need to worry about: 但是,由于张量往往只有几个维度,我认为只需要编写一些用户最有可能需要担心的情​​况就足够了:

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>

I know that this leads to some pretty ' WET ' code, but the cost of maintaining this code is still probably less than the cost of maintaining a custom arithmetic system like what I described above. 我知道这会导致一些漂亮的“ WET ”代码,但维护此代码的成本仍然可能低于维护自定义算术系统的成本,如上所述。

Note that official TypeScript declaration files often use patterns a bit like this (see lib.esnext.array.d.ts ). 请注意,官方TypeScript声明文件通常使用类似这样的模式(请参阅lib.esnext.array.d.ts )。 Only the most common use cases are covered with strongly typed definitions. 只有最常见的用例才包含强类型定义。 For any other use cases, the user is expected to provide type annotations/assertions where appropriate. 对于任何其他用例,期望用户在适当的地方提供类型注释/断言。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM