0

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?

2 Answers 2

3

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!

Sign up to request clarification or add additional context in comments.

Comments

0

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]> };

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.