Update: TypeScript 4.0 is set to introduce variadic tuple types, offering enhanced flexibility in manipulating built-in tuples. The Push<T, V>
operation will now be simplified as [...T, V]
. This results in a straightforward implementation as shown in the following code snippet:
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]
Playground link
For TypeScript versions older than v4.0:
Unfortunately, there is no officially supported method in TypeScript to represent the operation of appending a type to the end of a tuple. This operation, called Push<T, V>
, where T
is a tuple and V
is a value type, cannot be directly achieved. However, there is a way to prepend a value to the beginning of a tuple, known as Cons<V, T>
. This is due to a feature introduced in TypeScript 3.0 to treat tuples as function parameter types. Additionally, the type Tail<T>
can be used to extract the rest of the tuple after removing the first element (head):
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;
In attempting to represent Push
, there emerges a complicated recursive definition that is not feasible due to circular references. Although it is possible to work around this limitation, it is not recommended by the TypeScript team. Instead, limiting the tuple size to a fixed number (e.g. 9 or 10) and manually defining the Push
operation for that size is a more practical approach:
type Push<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push1<Tail<T>, V>>
...
By unrolling the recursive definition, a workaround can be implemented to achieve the desired tuple manipulation. This allows for adding elements to a tuple without exceeding the limitations imposed by TypeScript.
Equipped with the Push
operation, a type definition for loader
can be provided (implementation left to the user):
type Loader<T extends any[]> = {
add<V>(x: V): Loader<Push<T, V>>;
run(): T
}
declare const loader: Loader<[]>;
Testing the functionality:
var result = loader.add(1).add("hello").add(true).run(); //[number, string, boolean]
Successful operation is observed. Best of luck applying this workaround to achieve the desired tuple manipulation!
Update
Note that the previous solution relies on the --strictFunctionTypes
compiler flag. An alternative definition of Push
is provided below for scenarios where this flag cannot be used:
type PushTuple = ...
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;
This definition leverages mapped tuples introduced in TypeScript 3.1, offering a concise solution for small tuple sizes. However, it incurs a quadratic growth in repetition for larger tuple sizes. The choice between the two definitions depends on the specific requirements and constraints of the project.
Choose the approach that best suits your needs. Good luck with your TypeScript endeavors!