3
votes

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.

3

3 Answers

5
votes

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.

0
votes

You were really close. The problem here is that string is assignable to {}.

Demonstration of this fact here.

If you check for the string, number, etc. first before {}, then you can get what you want:

type ConvertToTest<T> = {
    [P in keyof T]: T[P] extends any[]
        ? ConvertToTest<T[P]>
        : T[P] extends string
        ? Test
        : T[P] extends number
        ? Test
        : T[P] extends boolean
        ? Test
        : ConvertToTest<T[P]>
}
0
votes

changing the type to

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

(extends string instead of extends {}) or

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

(extends object instead of extends {})

seems to do the trick.