最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

typescript - Change the type of a single deeply nested type within an object - Stack Overflow

programmeradmin0浏览0评论

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.

Share Improve this question asked Mar 12 at 11:15 myolmyol 10k24 gold badges97 silver badges158 bronze badges 2
  • 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
Add a comment  | 

1 Answer 1

Reset to default 1

The 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

发布评论

评论列表(0)

  1. 暂无评论