Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Declare arbitrarily nested array (recursive type definition)

Say I have a function like:

const nested = function(v: string | Array<string> | Array<Array<string>>){...}

the problem is v might be nested 5 or 6 levels deep. How can I declare a type that's arbitrarily nested?

For example, how can I handle:

nested([['zam'],[[[['zimm']]]]])

1 Answers

You can fairly easily describe an arbitrarily nested array type, like this:

interface NestedArray<T> extends Array<T | NestedArray<T>> { }

That type of recursive reference is allowed (where a type alias does not) because the evaluation of an interface's base type is deferred.

Unfortunately working with it is not straightforward. You can create values of such a type:

// works as expected
const nums: NestedArray<number> = [1,[2,[3,[4,[5],6,[7]],[8]],[[9]]]];

// errors as expected
const oops: NestedArray<number> = [1,[2,["3",[4,[5],6,[7]],[8]],[[9]]]]; // error

but the compiler gives up checking such nested types after something like five levels deep:

// no error!  
const what: NestedArray<number> = [[[[["a"]]]]]; // 😮

Also, you can't easily infer the ultimate element type given the array type. For example:

declare function doesntWork<T>(arr: NestedArray<T>): T;
const t = doesntWork([[1,2,3]]) ; // T is number[] | ConcatArray<number[]>; 🙁

You might expect T to be inferred as number, but the compiler is under no obligation to do so, since [[1,2,3]] is both a NestedArray<number[]> and a NestedArray<number>. Even if you try to force NestedArray<T> to only accept a T which is not an array, the compiler will not infer the element type the way you want.

If you need to infer the nested element type, you'll find yourself wanting to create a recursive type alias, probably involving conditional types. But you can't do that in TypeScript (as of 3.2 anyway).

type NestedElementType<T> = T extends Array<infer A> ? NestedElementType<A> : T; // error 🙁

The best you can do is pick a depth to support (say, 10 levels) and then unroll the recursive type alias:

type NestedElementType<T> = T extends Array<infer A> ? NET1<A> : T;
type NET1<T> = T extends Array<infer A> ? NET2<A> : T;
type NET2<T> = T extends Array<infer A> ? NET3<A> : T;
type NET3<T> = T extends Array<infer A> ? NET4<A> : T;
type NET4<T> = T extends Array<infer A> ? NET5<A> : T;
type NET5<T> = T extends Array<infer A> ? NET6<A> : T;
type NET6<T> = T extends Array<infer A> ? NET7<A> : T;
type NET7<T> = T extends Array<infer A> ? NET8<A> : T;
type NET8<T> = T extends Array<infer A> ? NET9<A> : T;
type NET9<T> = T extends Array<infer A> ? NETX<A> : T;
type NETX<T> = T extends Array<infer A> ? unknown : T; // bail out

That will work:

declare function doesWork<N extends NestedArray<any>>(arr: N): NestedElementType<N>;
const w = doesWork([[1,2,[3],[[4]]]]) ; // returns number 🙂

Given all those caveats you can possibly use this type:

function flatten<N extends NestedArray<any>>(arr: N): Array<NestedElementType<N>> {
  const ret: Array<NestedElementType<N>> = [];
  arr.forEach(l => {
    if (Array.isArray(l)) {
      ret.push(...flatten(l));
    } else {
      ret.push(l);
    }
  });
  return ret;
}

const flattened = flatten([["a"], ["b"], [[[[["c"]]], "d"]]]); // string[]

It's up to you if it's worth it. Hope that helps; good luck!

like image 163
jcalz Avatar answered Nov 01 '25 11:11

jcalz