Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Recursive conditional types

Tags:

typescript

I'd like to map an object recursively so that the primitive values in the object are converted to some other type.

For example, I'd like an object like this:

const before = { a: { c: '' }, b: [ '', { d: '' } ] }

to become this:

const after = { a: { c: Test }, b: [ Test, { d: Test } ] }

I'm also assuming that values won't be Date, Symbol, or null/void. Just JSON serializable types like string, numbers, etc. (except null)

Here's what I tried:

type ConvertToTest<T> = {
    [P in keyof T]: T[P] extends any[]
        ? ConvertToTest<T[P]>
        : T[P] extends {}
            ? ConvertToTest<T[P]>
            : Test;
}

function convert<T>(o: T): ConvertToTest<T> {
    // ...
}

This is using the conditional types introduced in Typescript 2.8.

const after = convert(before) results in after.a.c with string type completions in the editor for c, instead of completions for Test.

How do I rewrite type ConvertToTest<T> to convince Typescript that after.a.c is of type Test?

EDIT: Here's a Typescript Playground link illustrating the above.

like image 822
Kevin Beal Avatar asked Nov 24 '25 09:11

Kevin Beal


1 Answers

So you need two things from the ConvertToTest<T> type. One is that if T is a primitive type, then CovertToTest<T> = Test. The other is that if T isn't primitive, you want to keep the same keys but convert their values.

To do that, I'd just add the first case as one part of a conditional type, and then have the other branch use a recursive mapped type:

type Primitive = string | number | boolean | null | undefined;
type ConvertToTest<T> = T extends Primitive ? Test : {
    [K in keyof T]:
        T[K] extends (infer U)[] ? ConvertToTest<U>[] :
        ConvertToTest<T[K]>;
}

Using that, you can then use it like so:

// For example. Replace with whatever your actual type is.
type test = {
    foo(): string;
}

declare function convertToTest<T>(obj: T): ConvertToTest<T>;
const test = convertToTest({ a: "", b: { c: true, primArr: [1, ""], objArr: [{inner: ""}] } });

test.a.foo(); // OK
test.b.c.foo(); // OK
test.b.primArr[0].foo() // OK
test.b.objArr[0].inner.foo() // OK

This is a nice way to do it since it will work for objects of any depth, and will properly handle converting the elements of an array type as well.

like image 150
CRice Avatar answered Nov 28 '25 16:11

CRice