Remix.run Logo
epolanski 10 hours ago

The version I was thinking when I wrote the comment is simpler

    type Flatten<T> = T extends Array<infer U> ? Flatten<U> : T
> The average Typescript dev likely doesn't need to understand recursive conditional types.

The average X dev in Y language doesn't need to understand Z is a poor argument in the context of writing better software.

furyofantares 9 hours ago | parent | next [-]

> The average X dev in Y language doesn't need to understand Z is a poor argument in the context of writing better software.

It's a good response to the claim that we'd be surprised at how many would fail to do this, though.

NooneAtAll3 9 hours ago | parent | prev [-]

as a person that never touched JS and TS... what's the difference between the two answers?

granzymes 9 hours ago | parent | next [-]

For one, the simple answer is incomplete. It gives the fully unwrapped type of the array but you still need something like

  type FlatArray<T extends unknown[]> = Flatten<T[number]>[]
The main difference is that the first, rest logic in the complex version lets you maintain information TypeScript has about the length/positional types of the array. After flattening a 3-tuple of a number, boolean, and string array TypeScript can remember that the first index is a number, the second index is a boolean, and the remaining indices are strings. The second version of the type will give each index the type number | boolean | string.
ameliaquining 9 hours ago | parent | prev | next [-]

First one flattens a potentially-nested tuple type. E.g., FlatArr<[number, [boolean, string]]> is [number, boolean, string].

Second one gets the element type of a potentially-nested array type. E.g., Flatten<number[][]> is number.

For what it's worth, I've never needed to use either of these, though I've occasionally had other uses for slightly fancy TypeScript type magic.

pcthrowaway 4 hours ago | parent | prev [-]

The answer above actually gets the type union of all non-array elements of a multi-level array.

In other words

    Flatten<[1,[2,'a',['b']]]>
will give you a union type of 1, 2, 'a', and 'b'

    const foo: Flatten<[1,[2,'a',['b']]]> = 'b'; // OK
    const bar: Flatten<[1,[2,'a',['b']]]> = 'c'; // Error: Type '"c"' is not assignable to type '1 | 2 | "a" | "b"'
Technically the inference is unnecessary there, if that's you're goal:

     type Flatten<T> = T extends Array<unknown> ? Flatten<T[number]> : T
I don't really consider this the type of flattening an array, but `Array<Flatten<ArrType>>` would be. And this would actually be comparable to the builtin Array.prototype.flat type signature with infinite depth (you can see the typedef for that here[1], but this is the highest level of typescript sorcery)

My solution was for flattening an array with a depth of 1 (most people using Array.prototype.flat are using this default depth I'd wager):

    console.log(JSON.stringify([1,[2, [3]]].flat()));
    > [1,2,[3]]
The type I provided would match those semantics:

    // 'readonly' added to the 'extends' sections to work on "as const" (readonly) arrays
    type FlatArr<Arg extends unknown[] | readonly unknown[]> = Arg extends readonly [infer First, ...(infer Rest)] ?
      First extends readonly unknown[] ?
        [...First, ...FlatArr<Rest>] :
        [First, ...FlatArr<Rest>] :
      [];

    const flatten = <Arr extends unknown[] | readonly unknown[]>(arr: Arr): FlatArr<Arr> => arr.flat() as FlatArr<Arr>
    const someArr = [1,[2,'a',['b', ['c']]]] as const; // const someArr: readonly [1, readonly [2, "a", readonly ["b", readonly ["c"]]]]
    const someArr2 = flatten(someArr);                 // const someArr2: [1, 2, "a", readonly ["b", readonly ["c"]]]

    
[1]: https://github.com/microsoft/TypeScript/blob/main/src/lib/es...