You can do that with a recursive mapped type that looks like this:
type ReplaceTypes<ObjType extends object, FromType, ToType> = {
[KeyType in keyof ObjType]: ObjType[KeyType] extends object
? ReplaceTypes<ObjType[KeyType], FromType, ToType> // Recurse
: ObjType[KeyType] extends FromType // Not recursing, need to change?
? ToType // Yes, change it
: ObjType[KeyType]; // No, keep original
}
So for instance, if you had example: HumanString, example.age and example.animals[number].age would both be string instead of number.
It looks to see if the type of each property extends object and if so, recurses; otherwise, it looks to see if the type extends the "from" type and replaces it if so.
Playground link
All of those repeated instances of ObjType[KeyType] get tiresome, so you could split it up into two parts:
type ReplaceType<Type, FromType, ToType> =
Type extends object
? ReplaceTypes<Type, FromType, ToType>
: Type extends FromType
? ToType
: Type;
type ReplaceTypes<ObjType extends object, FromType, ToType> = {
[KeyType in keyof ObjType]: ReplaceType<ObjType[KeyType], FromType, ToType>;
}
Playground link
If you wanted to be able to do this with FromType being an object, type, it should work if you change the order:
type ReplaceType<Type, FromType, ToType> =
Type extends FromType // FromType?
? ToType // Yes, replace it
: Type extends object // Recurse?
? ReplaceTypes<Type, FromType, ToType> // Yes
: Type; // No, leave it alone
type ReplaceTypes<ObjType extends object, FromType, ToType> = {
[KeyType in keyof ObjType]: ReplaceType<ObjType[KeyType], FromType, ToType>;
}
Playground link