So I have this helper function that allows me to replace types based on structural matching:
type Replace<T, C, A> = {
[P in keyof T]: T[P] extends C ? A : T[P]
}
This allows me to do the following:
type NumberThing = { value: number }
type StringThing = Replace<NumberThing, number, string>
const a: StringThing = { value: "cenas" }
All is nice and dandy, until someone does this:
type ArrayOfNumberThing = { value: Array<number> }
Ok, so, I just add a new condition...
type Replace<T, C, A> = {
[P in keyof T]: T[P] extends C ? A : (T[P] extends Array<C> ? Array<A> : T[P])
}
And it types:
type ArrayOfNumberThing = { value: Array<number>, simpleValue: number }
type ArrayOfStringThing = Replace<ArrayOfNumberThing, number, string>
const b: ArrayOfStringThing = { value: ["cenas"], simpleValue: "still works" }
But this guy is stubborn, and now throws me a:
type CrazyNumberThing = { value: Array<Array<Array<number>>> }
Well, I could always do this:
type RecursiveArrayReplace<T, C, A> = T extends C ? A : (T extends Array<infer E> ? RecursiveArrayReplace<E, C, A> : T)
... which would, obviously, search deeply into the Array until it finds what it wants to replace, right? Right? Wrong:
Type alias 'RecursiveArrayReplace' circularly references itself.
And before I could wipe my tears, someone just threw me a:
type TupleStringNumberThing = { value: [string, number] }
... which is making me curl up in a fetal position unless you guys help me :(
Given your particular examples, I'd write it like this:
type DeepReplace<T, C, A> = T extends C ? A : T extends object ? {
[P in keyof T]: DeepReplace<T[P], C, A> } : T;
This should work for all your types including arrays/tuples if you're using TS3.1 or above:
type NumberThing = { value: number }
type StringThing = DeepReplace<NumberThing, number, string>
// type StringThing = { value: string; }
type ArrayOfNumberThing = { value: Array<number>, simpleValue: number }
type ArrayOfStringThing = DeepReplace<ArrayOfNumberThing, number, string>;
// type ArrayOfStringThing = { value: string[]; simpleValue: string; }
type CrazyNumberThing = { value: Array<Array<Array<number>>> };
type CrazyStringThing = DeepReplace<CrazyNumberThing, number, string>;
// type CrazyStringThing = { value: string[][][]; }
type TupleStringNumberThing = { value: [string, number] };
type TupleStringStringThing = DeepReplace<TupleStringNumberThing, number, string>;
// type TupleStringStringThing = { value: [string, string]; }
Obviously you could find other edge cases... should functions that accept/return C
should be with functions that accept/return A
? These can likely be handled but it might not be worth the extra complexity if your use case doesn't need it.
Okay, hope that helps; good luck!