tl;dr: I want to strongly type the following.
const foo = [ 'a' ] as const;
const bar = [ 1 ] as const;
const baz = [ true ] as const;
const concatted = foo.concat(bar, baz);
type Concatted = typeof concatted; // expect ['a', 1, true]
I've figured out how to add definitions for 0..n arguments, but I want to do it for an arbitrary number, preferably with one or two definitions.
Suppose I have:
const strArray = [ 'a' ] as const;
const numArray = [ 1 ] as const;
const concatenated = strArray.concat(numArray);
We know that concatenated is exactly equal to ['a', 1]
. I've figured out how to write a type definition for concat()
that gives us this.
declare global {
interface ReadonlyArray<T> {
concat<
A extends ReadonlyArray<T>,
I extends ReadonlyArray<unknown>
>(this: A, items: C): [...A, ...I];
}
}
type Concatenated = typeof concatenated; // => ['a', 1]
However, JavaScript's Array.concat()
takes in an arbitrary number of arrays. So now, let's do
const strArray = [ 'a' ] as const;
const numArray = [ 1 ] as const;
const boolArray = [ true ] as const;
const concatenated = strArray.concat(numArray, boolArray); // => [ 'a', 1, true ]
Prior to TypeScript 4's variadic tuples, the solution was something like
declare global {
interface ReadonlyArray<T> {
concat<
A extends ReadonlyArray<T>,
I extends ReadonlyArray<unknown>
>(this: A, items: I): [...A, ...I];
concat<
A extends ReadonlyArray<T>,
I1 extends ReadonlyArray<unknown>,
I2 extends ReadonlyArray<unknown>
>(this: A, item1: I1, item2: I2): [...A, ...I1, ...I2];
// ...additional concat() definitions through I_n...
}
}
I was hoping that with TypeScript 4, I could do something simpler
declare global {
interface ReadonlyArray<T> {
concat<
A extends ReadonlyArray<T>,
I extends ReadonlyArray<ReadonlyArray<unknown>>
>(this: A, ...items: I): [...A, ...(...I)];
}
}
This apparently doesn't work. I'm guessing there's some black magic using the
((val: ReadonlyArray<ReadonlyArray<unknown>>) => void) extends ((val: [infer U, ...infer R]) => void)
? [...U, ...<something something recurse with R>]
: never
pattern that I've never gotten the hang of, perhaps in tandem with magic using Extract<>
as seen in this answer.
It would be very nice if types could be recursive without any magic. Then I could easily write:
type Concat<T extends ReadonlyArray<ReadonlyArray<unknown>>> =
T extends [ReadonlyArray<unknown>, ...infer U]
? [...T[0], ...Concat<U>]
: [];
interface ReadonlyArray<T> {
concat<
A extends ReadonlyArray<T>,
I extends ReadonlyArray<ReadonlyArray<unknown>>
>(this: A, ...items: I): [...A, ...Concat<I>];
}
As an aside, I've never understood why recursive types--as long as they resolve within n depths--aren't supported. Infinite recursion, of course, would be bad/impossible, but I would think this syntax could be supported for tuples of size 100 or less pretty easily/efficiently.
Official support for recursive conditional types will be added in Typescript 4.1
With it in place we'll be able to:
type Concat<T> =
T extends [infer Head, ...infer Tail]
? Head extends ReadonlyArray<unknown>
? [...Concat<Head>, ...Concat<Tail>] : [Head, ...Concat<Tail>]
: T
The above definition allows to pass arrays and/or values to concatenate into a new array (just like regular concat)
The recursion limit is 50 nested type instantiations