Reputation: 3186
I would like to do the following:
var result = loader
.add<number>(1)
.add<string>("hello")
.add<boolean>(true)
.run();
I would like to construct this theoretical loader
object in such a way to have the TYPE of result be [number, string, boolean]
without needing to manually declare it as such. Is there a way to do this in TypeScript?
Upvotes: 3
Views: 123
Reputation: 328262
UPDATE: TypeScript 4.0 will feature variadic tuple types, which will allow more flexible built-in tuple manipulation. Push<T, V>
will be simply implemented as [...T, V]
. Therefore the entire implementation turns into the following relatively straightforward bit of code:
type Loader<T extends any[]> = {
add<V>(x: V): Loader<[...T, V]>;
run(): T
}
declare const loader: Loader<[]>;
var result = loader.add(1).add("hello").add(true).run(); //[number, string, boolean]
FOR TS before v4.0:
There is unfortunately no supported way in TypeScript to represent the type operation of appending a type onto the end of a tuple. I'll call this operation Push<T, V>
where T
is a tuple and V
is any value type. There is a way to represent prepending a value onto the beginning of a tuple, which I'll call Cons<V, T>
. That's because in TypeScript 3.0, a feature was introduced to treat tuples as the types of function parameters. We can also get Tail<T>
, which pulls the first element (the head) off a tuple and returns the rest:
type Cons<H, T extends any[]> =
((h: H, ...t: T) => void) extends ((...r: infer R) => void) ? R : never;
type Tail<T extends any[]> =
((...x: T) => void) extends ((h: infer A, ...t: infer R) => void) ? R : never;
Given Cons
and Tail
, the natural representation of Push
would be this recursive thing that doesn't work:
type BadPush<T extends any[], V> =
T['length'] extends 0 ? [V] : Cons<T[0], BadPush<Tail<T>, V>>; // error, circular
The idea there is that Push<[], V>
should just be [V]
(appending to an empty tuple is easy), and Push<[H, ...T], V>
is Cons<H, Push<T, V>>
(you hold onto the first element H
and just push V
onto the tail T
... then prepend H
back onto the result).
While possible to trick the compiler into allowing such recursive types, it is not recommended. What I usually do instead is pick some maximum reasonable length of tuple I want to support modifying (say 9 or 10) and then unroll the circular definition:
type Push<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push1<Tail<T>, V>>
type Push1<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push2<Tail<T>, V>>
type Push2<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push3<Tail<T>, V>>
type Push3<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push4<Tail<T>, V>>
type Push4<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push5<Tail<T>, V>>
type Push5<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push6<Tail<T>, V>>
type Push6<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push7<Tail<T>, V>>
type Push7<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push8<Tail<T>, V>>
type Push8<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push9<Tail<T>, V>>
type Push9<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], PushX<Tail<T>, V>>
type PushX<T extends any[], V> = Array<T[number] | V>; // give up
Each line except PushX
looks just like the recursive definition, and we intentionally cut things off at PushX
by giving up and just forgetting about the order of elements (PushX<[1,2,3],4>
is Array<1 | 2 | 3 | 4>
).
Now we can do this:
type Test = Push<[1, 2, 3, 4, 5, 6, 7, 8], 9> // [1, 2, 3, 4, 5, 6, 7, 8, 9]
Armed with Push
, let's give a type to loader
(leaving the implementation up to you):
type Loader<T extends any[]> = {
add<V>(x: V): Loader<Push<T, V>>;
run(): T
}
declare const loader: Loader<[]>;
And let's try it:
var result = loader.add(1).add("hello").add(true).run(); //[number, string, boolean]
Looks good. Hope that helps; good luck!
The above only works with --strictFunctionTypes
enabled. If you must do without that compiler flag, you could use the following definition of Push
instead:
type PushTuple = [[0], [0, 0], [0, 0, 0],
[0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
];
type Push<
T extends any[],
V,
L = PushTuple[T['length']],
P = { [K in keyof L]: K extends keyof T ? T[K] : V }
> = P extends any[] ? P : never;
It's more terse for small supported tuple sizes, which is nice, but the repetition is quadratic in the number of supported tuples (O(n2) growth) instead of linear (O(n) growth), which is less nice. Anyway it works by using mapped tuples which were introduced in TS3.1.
It's up to you.
Good luck again!
Upvotes: 4