typescriptstatic-typing

How to statically type function that returns array 1 to n


I'm trying to make a function that returns an array of numbers, from 0 to n, but I want the returned type to also be the known array statically.

const makeFoo = (n: number) => [...Array(n).keys()];
const foo1 = [0, 1, 2] as const; // readonly [0, 1, 2]
const foo2 = makeFoo(3); // number[]
console.log(foo1); // [0, 1, 2]
console.log(foo2); // [0, 1, 2]

While foo1 and foo2, have the same value at runtime, their types are different.

How do I define makeFoo's return type to be like foo1? (readonly [0, 1, ... n] where n is also known statically e.g. makeFoo = <T extends number>(n: T) => ...)


Solution

  • You could use a recursive helper type which computes the return type based on a generic argument. However, you will need a type assertion for that (I don't think there's a way without) since [...Array(n).keys()] will always be inferred as number[].

    type MakeFoo<N extends number, R extends any[] = []> = R["length"] extends N
      ? R
      : MakeFoo<N, [...R, R["length"]]>;
    
    function makeFoo<N extends number>(n: N): MakeFoo<N> {
      return [...Array(n).keys()] as MakeFoo<N>
    }
    
    const foo3 = makeFoo(3);
    //    ^? const foo3: [0, 1, 2]
    const foo100 = makeFoo(100);
    //    ^? const foo100: [0, 1, 2, 3, 4, 5, 6, 7, ... , 99]
    

    TypeScript Playground


    Caveat: This only works for a maximum of 999 items. Anything above will resolve to any.

    const foo1000 = makeFoo(1000);
    //              ~~~~~~~~~~~~~
    // Type instantiation is excessively deep and possibly infinite.