Home > other >  Returning a nested array type in Typescript
Returning a nested array type in Typescript

Time:08-10

This question is much more academic than practical.

I have a function filledArray(val, ...dims), which takes a fill value and a variable number of dimensions, and builds a nested multidimensional array filled with that value, to the specification in the dimensions. For example:

> filledArray('x', 5)
[ 'x', 'x', 'x', 'x', 'x' ]

> filledArray('x', 2, 5)
[ [ 'x', 'x', 'x', 'x', 'x' ],
  [ 'x', 'x', 'x', 'x', 'x' ], ]

> filledArray('x', 2, 5, 1)
[ [ [ 'x' ], [ 'x' ], [ 'x' ], [ 'x' ], [ 'x' ] ],
  [ [ 'x' ], [ 'x' ], [ 'x' ], [ 'x' ], [ 'x' ] ] ]

Typescript infers that the return type of this function is any. Can the true return type be expressed in the type system somehow, so that the three return values above are correctly typed as string[], string[][], and string[][][]? (And so on, for deeper and deeper arrays?)

A sample implementation of this function:

function filledArray<T>(val: T, ...dims: number[]) {
    if (dims.length == 0)
        return val

    let [dim, ...rest] = dims
    return Array.from(Array(dim), _ => filledArray(val, ...rest))
}

Interestingly, it even seems like specifying a fixed number of overload declarations seems to make the final ...rest in the function somehow be of the wrong type...

CodePudding user response:

Such a return type can be expressed with a recursive conditional type.

type FilledArray<N extends number[], T> = 
  N extends [number, ...infer Rest extends number[]]
    ? FilledArray<Rest, T>[]
    : T

function filledArray<T, N extends number[]>(val: T, ...dims: N): FilledArray<N, T> {
  if (dims.length == 0)
    return val as unknown as FilledArray<N, T>

  let [dim, ...rest] = dims
  return Array.from(Array(dim), _ => filledArray(val, ...rest)) as FilledArray<N, T>
}

We can recursively check how many numbers the dims tuple holds and add a [] to the return type for each number.

The first conditional N extends [number, ...infer Rest extends number[]] checks if there is still at least one element in the tuple N and simultaneously stores every element except the first one in the inferred type Rest. Rest might also be an empty tuple if there are no more elements. If the conditional is true, we call the type recursively with Rest and add a [] to the end of it. If the conditional is false, we can return the type T to end the recursion.

Since TypeScript does not understand how conditional return types relate to the function implementation, we have to use type assertions for the returned values.

Here are some tests:

const a = filledArray('x', 5)
// const a: "x"[]

const b = filledArray('x', 2, 5)
// const b: "x"[][]

const c = filledArray(0, 2, 5, 1)
// const c: 0[][][]

Playground

  • Related