Obscure TypeScript: Variadic Tuple Types
Let’s say that you are writing a function that concatenates the contents of two arrays together, and you want to be able to express that somehow within the function’s types. For simplicity’s sake, let’s assume that both arrays contain only strings:
function concat(a: string[], b: string[]): string[];
While these types are technically correct, they don’t offer much assurance around the correctness of the output. For example, a programmer that doesn’t know any better could satisfy the checker by doing this:
function concat(a: string[], b: string[]): string[] {
return a;
}
The TypeScript compiler would happily compile this function, but there would be a pretty nasty logical error you’ll have to deal with. We could potentially address this by adding some overloads:
function concat(a: string[], b: string[]): string[];
function concat<A extends string>(a: [A], b: []): [A];
function concat<A extends string, B extends string>(a: [A, B], b: []): [A, B];
// And so on....
But as you can see, we would have to create overloads with generics for every possible combination, which would not scale at all. We would only be able to express correct types for the overloads that we write.
A better way of typing this would be to instead declare the output as an array that
contains elements from either a
or b
:
function concat<A, B>(a: readonly A[], b: readonly B[]): Array<A | B>;
const a = ["1", "2", "3"] as const;
const b = ["a", "b", "c"] as const;
const output = concat(a, b);
This comes with it’s own issues though, as we are not able to capture the length or
order of elements in the output array with this approach, as seen by the inferred type
of output
being ("1" | "2" | "3" | "a" | "b" | "c")[]
. So, is there a better way?
Variadic Tuple Types
Introduced in TypeScript 4.0, Variadic Tuple Types allow us to express such scenarios correctly:
function concat<
A extends readonly unknown[],
B extends readonly unknown[]k
>(a: A, b: B): [...A, ...B];
const a = ['1', '2', '3'] as const
const b = ['a', 'b', 'c'] as const
const output = concat(a, b)
Here, TypeScript understands statically that output
contains['1', '2', '3', 'a', 'b', 'c']
in that specific order thanks to the variadic
tuple return type [...A, ...B]
. This means that we are now able to work with tuples
and arrays, whose types are not statically known to us, succinctly and accurately.
Let’s look at another example – typing a curry
function (example taken from
here):
function curry<T extends unknown[], U extends unknown[], R>(
f: (...args: [...T, ...U]) => R,
...a: T
) {
return (...b: U) => f(...a, ...b);
}
function add(a: number, b: number): number {
return a + b;
}
const add10 = curry(add, 10);
const output = add10(20);
console.log(output); // 30
There’s a lot to unpack here, let’s take it step-by-step. Firstly, the curry
function
takes in two parameters, f
, which is the function we want to curry, and a
, which is
a rest element that allows us to provide an arbitrary number of arguments (but no more
than what f
accepts thanks to variadic tuple types).
To make the magic happen, we tell the checker that f
takes in an arbitrary number of
arguments using a variadic tuple type [...T, ...U]
. In simple terms, this type is
saying that the parameters to f
is a combination of two types, T
and U
, where T
is applied first and U
second. The arguments of T
are given to us when we first call
curry
, and the remaining arguments U
are given to us when we call the returned
thunk.