简体   繁体   中英

Typescript subtract two numbers at compile time

Original question

I need an utility type Subtract<A, B> where A and B are numbers. 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. 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. I have a multidimensional array, which has Dims dimensions. When a slice method is called, its dimensions are reduced.

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() .

If it is hard to make Subtract<A, B> type, is it possible to decrement type? 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. 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'] .

Now you can define some arithmetic concepts using array-based numerals (not 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. 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.

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. It also only seems to reliably work up to N<23> or so (probably due to limits on recursive types within 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.


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:

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.

Note that official TypeScript declaration files often use patterns a bit like this (see 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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