I have a type Example
with a deeply nested type with a single property target
I wish to change the type of.
Each "level" has many additional properties with types which I do not want to change (not shown for brevity)
type Example = {
level1: {
level2: {
level3: {
target: string
}
}
}
}
to
type Example = {
level1: {
level2: {
level3: {
target: "a" | "b"
}
}
}
}
I tried using the answers of this question as a basis, but I am not confident with creating TypeScript functions so didn't get very far.
I also tried using some "deepOmit" functions I found across the internet, along with type merging, but I believe type merging with nested properties won't work - level1 and all its properties would be completely replaced, not merged;
deepOmit<Example, "target"> & { level1: { level2: { level3: { target: "a" | "b" }}}}
I think that I need to use a recursive TypeScript function to 'search' for the parameter name and replace its type with "a" | "b"
, but I am not having much success.
I have a type Example
with a deeply nested type with a single property target
I wish to change the type of.
Each "level" has many additional properties with types which I do not want to change (not shown for brevity)
type Example = {
level1: {
level2: {
level3: {
target: string
}
}
}
}
to
type Example = {
level1: {
level2: {
level3: {
target: "a" | "b"
}
}
}
}
I tried using the answers of this question as a basis, but I am not confident with creating TypeScript functions so didn't get very far.
I also tried using some "deepOmit" functions I found across the internet, along with type merging, but I believe type merging with nested properties won't work - level1 and all its properties would be completely replaced, not merged;
deepOmit<Example, "target"> & { level1: { level2: { level3: { target: "a" | "b" }}}}
I think that I need to use a recursive TypeScript function to 'search' for the parameter name and replace its type with "a" | "b"
, but I am not having much success.
- These sorts of deeply recursive utility types tend to have bizarre edge cases. You can use the approach from this playground link and it works for your example, but I have no idea if it works for your actual use cases. Please test thoroughly. If it works, then I can write up an answer explaining. If not, then please edit the post to include examples that demonstrate the failing use cases. – jcalz Commented Mar 12 at 12:29
- From initial testing this appears to work in my use case. I understand the risks of edge cases for functions like this, but my specific use case has only one property uniquely named "target" so it appears to work as advertized – myol Commented Mar 12 at 13:33
1 Answer
Reset to default 1The simplest implementation I can think of looks like
type ReplaceNestedKey<T, K extends PropertyKey, V> =
{ [P in keyof T]: P extends K ? V : ReplaceNestedKey<T[P], K, V> };
where ReplaceNestedKey<T, K, V>
takes an input type T
, a key type K
, and a value type V
, and recursively replaces any property whose key is K
with the property value type V
. It uses a conditional type to check whether the current key P
matches the key K
to be replaced.
This might or might not display the type you expect with IntelliSense (you might just see ReplaceNestedKey
in the displayed type), so I'll augment it like
type ReplaceNestedKey<T, K extends PropertyKey, V> =
{ [P in keyof T]: P extends K ? V : ReplaceNestedKey<T[P], K, V> }
& ({} | undefined | null);
which intersects with an unknown
-like type that conceptually changes nothing about the shape, but in practice forces TypeScript to evaluate the type more eagerly.
Now let's see it on your example:
type Example = { level1: { level2: { level3: { target: string } } } }
type Replaced = ReplaceNestedKey<Example, "target", "a" | "b">;
/* type Replaced = {
level1: {
level2: {
level3: {
target: "a" | "b";
};
};
};
} */
That's what you wanted to see. So it works for your example code.
Still, any kind of recursive mapped type with conditional types will tend to have bizarre edge cases. It might not behave as expected for types with optional properties, index signatures, methods, unions, intersections, arrays, etc. And sometimes the refactoring necessary to address edge cases can be quite substantial (if it is even possible). So it's important, especially for future readers, to thoroughly test any such type against a wide range of important use cases.
Playground link to code