CRice
CRice

Reputation: 32176

Convert nested array type

I'm looking for a type that can be used to convert a nested array type of arbitrary depth to another type. So some type, ConvertToBool<T> such that:

ConvertToBool<number[][][]> // should be boolean[][][]

I managed to get this working for non-nested array types, and for object types. I even wrote this answer for converting the fields of an object type, and it works well except for the case of nested arrays.

Now I wanted to write a type like this:

type Primitive = string | number | boolean | null | undefined;

type ConvertToBool<T> =
    T extends Primitive ? boolean :
    T extends (infer U)[] ? ConvertToBool<U>[] : // <-- Problem Line
    {[K in keyof T]: ConvertToBool<T[K]>};

But since a type alias cannot circularly reference itself, this doesn't work. Does anyone know of a workaround to automatically convert the type of an arbitrarily nested array type?

Upvotes: 0

Views: 453

Answers (2)

Mohammed Essehemy
Mohammed Essehemy

Reputation: 2166

I have used the same technique @jcalz (thank you) mentioned to convert any Date to a string and it worked very good

    type PrimitiveType = string | number | boolean | null | undefined | never;
    export type ConvertDateToString<T> =
       T extends Date ? string :
       T extends PrimitiveType ? T :
       T extends Array<infer U> ? Array<ConvertDateToString<U>> :
       { [K in keyof T]: ConvertDateToString<T[K]> };

Upvotes: 0

jcalz
jcalz

Reputation: 328262

Yeah, this is a known limitation, but it specifically has a workaround for anything like Array<> which is an interface. See the definition of DeepReadonly<T> from @ahejlsberg's pull request introducing conditional types:

type DeepReadonly<T> =
    T extends any[] ? DeepReadonlyArray<T[number]> :
    T extends object ? DeepReadonlyObject<T> :
    T;

interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}

type DeepReadonlyObject<T> = {
    readonly [P in NonFunctionPropertyNames<T>]: DeepReadonly<T[P]>;
};

As mentioned in the text:

Similar to union and intersection types, conditional types are not permitted to reference themselves recursively (however, indirect references through interface types or object literal types are allowed [emphasis mine], as illustrated by the DeepReadonly<T> example above).


So, for your case, we can do this:

type Primitive = string | number | boolean | null | undefined;

interface ConvertToBoolArray<T> extends Array<ConvertToBool<T>> {}

type ConvertToBool<T> =
  T extends Primitive ? boolean :
  T extends (infer U)[] ? ConvertToBoolArray<U> : // okay now
  { [K in keyof T]: ConvertToBool<T[K]> };

And let's see it work:

declare const c: ConvertToBool<{ a: number, b: string[], c: { d: number }[] }>;
c.a // boolean
c.b[2] // boolean
c.c[3].d // boolean

type BoolBoolBool = ConvertToBool<number[][][]>
declare const bbb: BoolBoolBool;
bbb[0][1][2] === true; // okeydokey

This works, with the caveat that BoolBoolBool doesn't look like boolean[][][] when you inspect it; it is ConvertToBoolArray<number[][]>, but those are equivalent:

type IsSame<T extends V, U extends T, V=U> = true;
// IsSame<T, U> only compiles if T extends U and U extends T:

declare const sameWitness: IsSame<BoolBoolBool, boolean[][][]> // works

Hope that helps; good luck!

Upvotes: 3

Related Questions