TypeScript Version: 4.x
Search Terms: flat, flatMap, flat array
Code
const array = [
[
'a',
'b',
] as const,
[
'c',
'd',
] as const,
] as const;
const flattenArray = array.flat();
Expected behavior:
Const flattenArray should be a type of '["a", "b", "c", "d"]'.
Actual behavior:
Const flatterArray is a type of '("a" | "b" | "c" | "d")[]'.
I think in this moment there is no way to keep concrete value of flatten array. only, not pretty way, which I found, is a creating type, which manual rewrites values from multidimensional array to one-dimensional array:
https://www.typescriptlang.org/play?noImplicitAny=false&strictFunctionTypes=false&strictPropertyInitialization=false&strictBindCallApply=false&noImplicitThis=false&noImplicitReturns=false&alwaysStrict=false&esModuleInterop=false&emitDecoratorMetadata=false&target=99&ts=4.0.2#code/C4TwDgpgBAYgNgQ2MCA7AggJ0wkAeAFSggA8VUATAZykwgQoHtU4QoAKOh51qBVEAG0AugEoRAPigBeKIIBQUJVAKCADMPXCANIuWqNggIw69Sg5oBMp5SuOaNu26pP2n++4OvvzXhzY9rNzM7IO95YQBueXkAY2YqYD5sXBk5EIVbWwByBGyfLOyAI3yQ4T4aeNREgsyspWzY0vqGimblcoRKhOBdTu7q4Gi4nqgAM0RgLBwQAC5YSfJp3DxQSEYx5JmpWQQUkAA6CaR2UQqFpCX91fAIDa3cCUigA
Playground Link: https://www.typescriptlang.org/play?noImplicitAny=false&strictFunctionTypes=false&strictPropertyInitialization=false&strictBindCallApply=false&noImplicitThis=false&noImplicitReturns=false&alwaysStrict=false&esModuleInterop=false&emitDecoratorMetadata=false&target=99&ts=4.0.2#code/MYewdgzgLgBAhgJwXAnjAvDA2gKBv7PA4gcjhIBojj8SAjS6mAXXghlEiiuNxoJLBG-WgBNhBVnHadoVKTPDQA3DhyzYAMwA2cKFACmYAIJJUGeGZQA6HXoAUASlVA
We don't really have a good way in the type system to represent this, I think. The produced type is still correct, albeit less specific than it could be.
Since that signature is already using recursion, you can get a more specific type:
type Flat1<A extends readonly unknown[]> = ReturnType<
A extends readonly [infer U, ...infer V] ?
U extends readonly unknown[] ?
() => [...U, ...Flat1<V>]
: () => [U, ...Flat1<V>]
: () => A
>;
type Flat<Arr, Depth extends number> = {
"done": Arr,
"recur": Arr extends readonly unknown[]
? Flat<Flat1<Arr>, [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20][Depth]>
: Arr
}[Depth extends 0 ? "done" : "recur"];
interface ReadonlyArray<T> {
flat<A, D extends number = 1>(
this: A,
depth?: D
): Flat<A, D>
}
Hmm... I found that thing: [...T]; Maybe it could be used to represent this? Now I can do something like that:
type FlattenArray<T extends readonly (readonly unknown[])[]> = [
...T[0],
...T[1],
...T[2],
];
Maybe that system should be extended to something like that: "...(...T)"? I think it's similiar to "readonly (readonly unknown[])[]" syntax and it's not broking previous concepts. Finally, FlattenArray type could looks like:
type FlattenArray<T extends readonly (readonly unknown[])[]> = [
...(...T),
];
Since that signature is already using recursion, you can get a more specific type:
type Flat1<A extends readonly unknown[]> = ReturnType< A extends readonly [infer U, ...infer V] ? U extends readonly unknown[] ? () => [...U, ...Flat1<V>] : () => [U, ...Flat1<V>] : () => A >; type Flat<Arr, Depth extends number> = { "done": Arr, "recur": Arr extends readonly unknown[] ? Flat<Flat1<Arr>, [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20][Depth]> : Arr }[Depth extends 0 ? "done" : "recur"]; interface ReadonlyArray<T> { flat<A, D extends number = 1>( this: A, depth?: D ): Flat<A, D> }
Wow... I like it.
If TypeScript allows to do this, maybe it should be implemented? On the other hand using ReturnType and functions probably it's not code clear enough.
In the nightly you don't need the ReturnType workaround anymore~
There's a bug in that code, it doesn't handle rest-elements. So Flat<[ 1, ...[2][]], 1> doesn't produce [1, ...(2)[]] as expected.
Switched to ts-nightly and fixed the bug
type Flat1<A extends readonly unknown[]> =
A extends readonly [infer U, ...infer V] ?
U extends readonly unknown[] ?
[...U, ...Flat1<V>]
: [U, ...Flat1<V>]
: A extends readonly (infer U)[] ?
(U extends readonly (infer V)[] ? V : U)[]
: A
type Flat<Arr, Depth extends number> = {
"done": Arr,
"recur": Arr extends readonly unknown[]
? Flat<Flat1<Arr>, [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20][Depth]>
: Arr
}[Depth extends 0 ? "done" : "recur"];
declare global {
interface ReadonlyArray<T> {
flat<A, D extends number = 1>(
this: A,
depth?: D
): Flat<A, D>
}
interface Array<T> {
flat<A, D extends number = 1>(
this: A,
depth?: D
): Flat<A, D>
}
}
I reworked it a little bit
type _Flat<A extends readonly unknown[]> =
// Empty Array case
A extends [] ? [] : A extends readonly [] ? readonly []
// Tuple case
: A extends readonly [infer U, ...infer V]
? A extends unknown[]
// Mutable tuple case
? U extends readonly unknown[]
? [...U, ..._Flat<V>]
: [U, ..._Flat<V>]
// Readonly tuple case
: U extends readonly unknown[]
? readonly [...U, ..._Flat<V>]
: readonly [U, ..._Flat<V>]
// Array case
: A extends (infer U)[]
? U extends readonly (infer V)[]
? V[] : U[]
// ReadonlyArray case
: A extends readonly (infer U)[]
? U extends readonly (infer V)[]
? readonly V[] : readonly U[]
: never
type Flat<Arr, Depth extends number, Iter extends readonly void[] = []> = {
"done": Arr,
"recur": Arr extends readonly unknown[]
? Flat<_Flat<Arr>, Depth, [void, ...Iter]>
: Arr
}[Iter['length'] extends Depth ? "done" : "recur"]
declare global {
interface ReadonlyArray<T> {
flat<A, D extends number = 1>(
this: A,
depth?: D
): Flat<A, D>
}
interface Array<T> {
flat<A, D extends number = 1>(
this: A,
depth?: D
): Flat<A, D>
}
}
What's the reasoning behind splitting the conditionals into Readonly/ReadWrite Arrays? Shouldn't Array extend ReadonlyArray, so you don't need to split them?
@RyanCavanaugh What with that issue? Any plans to add one the above propositions?
The minimal drop in precision here doesn't seem to outweigh the complexity of the proposed solutions.
Hmm... I have a different opinion about this. Your argument "drop in precision" also could be used in moment, when flat() would return "unknown[]".
I don't get it, why I have perfect precision in declared array, but when I flatten that array I lose it.
But solution given by @awerlogus is enough to me. If You think, that issue is not enough important to resolve - just close it - it will not be easy to me, but I think I will deal with it ;)
@swojdyga this solution has a lot of flaws (as any other TypeScript code that tries to not drop precision) and should be reworked (but I'm not sure if it is possible because of the language design). Some examples:
// Now: number[]
// Expected: [...(number[]), 4]
// ^^^ is not supported by TypeScript
type R1 = Flat<[Array<number>, 4], 1, []>
// Now: (number[] | 4)[]
// Expected: [Array<number>, 4]
type R2 = Flat<[Array<Array<number>>, 4], 1, []>
// Now: number[] | 4[]
// Expected: [...(number[]), 4]
// ^^^ is not supported by TypeScript
type R3 = Flat<[Array<Array<number>>, 4], 2, []>
So I think the precision drop is really better than this solution that can be so easily broken.