Ivan V.
Ivan V.

Reputation: 8091

Change type of values in deeply nested object structure

I need to recursively get through the data structure and create a type that has some of the fields changed to a different type, based on a condition.

Based on the following structure, I need to create a type (Result) where all A types are replaced with B types.

class A{}
class B{}

const data = {
    propA:new AA,
    propB:true,
    nested:{
        propC:new AA,
        propD:false
    }
}

// something like:
type Replace<typeof data, A, B>

// result
type Result = {
    propA:B,
    propB:boolean,
    nested:{
        propC:B
        propD:boolean
    }

}

Upvotes: 3

Views: 1900

Answers (2)

Joey Kilpatrick
Joey Kilpatrick

Reputation: 1602

This can be achieved with conditional, recursive types:

class A {
    ClassPropA!: string
}
class B {
    ClassPropB!: string
}

const data = {
    propA: new A(),
    propB: true,
    nested:{
        propC: new A(),
        propD:false
    }
}

type Replace<T, A, B> = T extends object
    ? { [key in keyof T]: T[key] extends A ? B : Replace<T[key], A, B> }
    : T;

// type Result = {
//     propA: B,
//     propB: boolean,
//     nested:{
//         propC: B
//         propD: boolean
//     }
//
// }
type Result = Replace<typeof data, A, B>;

Upvotes: 1

Oblosys
Oblosys

Reputation: 15116

You can do this with a mapped type, but keep in mind that matching is based on object structure rather than class name, so an object from a class C{} will also get converted when you're targeting A.

The Replace type can be defined as

type Replace<T, From, To> = T extends (...args: any[]) => any ? T : {
  [K in keyof T]: 
    [T[K], From] extends [From, T[K]] ? To : Replace<T[K], From, To>
}

The first condition is to preserve any methods/function properties as mapped types will convert these to {}. The mapped type itself processes each key, and checks whether both value extends the From type and the From type extends the value type, to ensure equality. If both are equal, the value gets replaced with To, and otherwise Replace is called recursively.

Here's an example conversion:

class A{}
class B{b = 42}
class C{}

const data = {
    propA: new A(),
    propBool: true,
    propC: new C(),
    nested:{
        propA: new A(),
        propBool: false
    },
    f: () => 42
}

type Result = Replace<typeof data, A, B>
// type Result = {
//     propA: B;
//     propBool: boolean;
//     propC: B;
//     nested: {
//         propA: B;
//         propBool: boolean;
//     };
//     f: () => number;
// }

TypeScript playground

Upvotes: 3

Related Questions