Search code examples
typescripttypescript-generics

Statically sized array with union parameter


I have an utility to create a statically sized array:

export type StaticArray<T, L extends number, R extends any[] = []> = R extends {
  length: L;
}
  ? R
  : StaticArray<T, L, [...R, T]>;

I can verify it works:

let myVar: StaticArray<number, 3>;
//  ^? let myVar: [number, number, number]

However, I want to make it work with sum types, but I'm not sure how it's possible. I would love this case to have that result:

let myVar: StaticArray<number, 3 | 2 | 1>;
//  ^? let myVar: [number, number, number] | [number, number] | [number]

But I doesn't seem to work, since my StaticArray type stops at the smallest number it sees.


Solution

  • You're saying that you want StaticArray<T, L> to distribute over unions in L, so that StaticArray<T, L1 | L2> is equivalent to StaticArray<T, L1> | StaticArray<T, L2>.

    The general way to do this is to use a distributive conditional type. When you write a conditional type of the form T extends U ? V : W where T is a generic type parameter, TypeScript will automatically distribute over unions in T. If all you want is to turn a non-distributive type into a distributive one, you can wrap it with a "no-op" distributive conditional type like T extends any ? ⋯ : never. That looks like it does nothing (T always extends any) but it has the effect of distributing over unions.

    So that leads us to write

    type StaticArray<T, L extends number, R extends any[] = []> =
      L extends any ? R extends { length: L; } ? R : StaticArray<T, L, [...R, T]> : never;
    

    and you can see that it works as desired:

    let myVar: StaticArray<number, 3>;
    //  ^? let myVar: [number, number, number]
    
    let myVar2: StaticArray<number, 3 | 2 | 1>;
    //  ^? let myVar2: [number] | [number, number] | [number, number, number]
    

    Note that the any in T extends any ? ⋯ : never can be replaced with anything were you're sure T will extend it. You can use unknown, or T, or some constraint (like number for L), depending on your preferences.

    Playground link to code