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!