Reputation: 32176
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
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
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