1

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 :(

2
  • Why isn't it written like this instead: type ArrayOfStringThing = Replace<ArrayOfNumberThing, Array<number>, Array<string>>? Commented Apr 23, 2020 at 18:44
  • Because that would be too easy ;) The point was to get an answer to an arbitrary deep type replacement problem. Commented Apr 23, 2020 at 22:28

1 Answer 1

4

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!

Playground link to code

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

Comments

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.