4

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
    }

}

2 Answers 2

3

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

Sign up to request clarification or add additional context in comments.

4 Comments

Why is it necessary to check that From extends T[K]?
@JoeyKilpatrick To avoid also replacing subtypes of From. If From is {a: 1} for example, then without the extra condition it would also replace {a:1, b:2}.
That is certainly a strength of your answer over my own. Also, I had never seen the use of tuples to effectively check multiple extends conditions together, very clever. Upvote from me.
Never knew you could check for multiple extends. Advanced Typescript looks like magic.
1

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>;

1 Comment

This answer is also correct, and it's easier to reason about, however, I'm going with the @Oblosys answer since it includes that additional check. So just an upvote from me this time.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.