If you want to achieve this with a mapped type, remember that the matching is done based on the object structure and not the class name. This means that an object from a class C{} will also be converted when targeting class A.
The definition of the Replace type can be written as follows:
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 initial condition is set to keep any methods or function properties intact as mapped types tend to change these to {}. The mapping type then goes through each key, checking if both the value extends the From type and the From type extends the value type for equality. If they are equal, the value is replaced with To, if not, the Replace function is called recursively.
Here's an example of how this conversion works:
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;
// }
Check out the TypeScript playground