Reputation:
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.
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>>
}
Upvotes: 5
Views: 1902
Reputation: 149020
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?
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.
Upvotes: 11