【问题标题】:Typescript: Cannot declare additional properties on Mapped types打字稿:无法在映射类型上声明其他属性
【发布时间】:2021-10-27 04:12:46
【问题描述】:

使用 TypeScript 键入对象时遇到问题

我声明了一个应用程序的某些文件使用的类型:

type Category = "cat1"|"cat2"|"cat3"|"cat4"

我使用Category 输入一个对象,一切正常使用:

const obj: {
  [key in Category]: string
} = {---}

但是现在,我想在对象上添加 2 个不是 Category 值的键。 我认为通过键入这样的对象会很容易:

const obj: {
  customKey1: string
  customKey2: string
  [key in Category]: string
} = {---}

但 TS 未按预期工作,而是发送 3 个错误:TS2464、TS1170 和 TS2693

A computed property name must be of type 'string', 'number', 'symbol' or 'any'. ts(2464)
'Category' only refers to a type, but is using as a value here. ts(2693)
A computed property name in a type template must refer to an expression whose type is a literal type or a 'unique symbol' type. ts(1170)

好吧...为什么? 由于Category 是通过文件共享的,我不想通过添加这些仅在此处需要的自定义键来编辑它。 我解决了这个问题,所以如果其他人面临这个问题,我会在下面给出一个解决方案,但是有人知道为什么我的第一个想法不能应用吗?
我不明白为什么单独写[key in Category]: string 有效,但如果添加另一个键则会抛出错误。

【问题讨论】:

    标签: typescript


    【解决方案1】:

    {[K in XXX]: YYY} 语法是 mapped type,这是一种特殊的对象类型,它遍历 keylike 类型表达式 XXX 的联合成员,并为 YYY 类型表达式中的每个成员使用类型参数 K。 (注意,K 表示 type,而不是属性键 value,因此按照惯例,我们在此处使用大写字符)。您可以在YYY 表达式中使用K,以便XXX 中的每个键K 可以具有不同的值类型,例如{[K in "a" | "b"]: K} 等价于{a: "a", b: "b"}映射类型语法不可推广或扩展;你不能像{[K in XXX]: YYY; somethingElse: string}那样把其他属性放在那里,你不能把它们放在interface声明中,或者用它做任何其他事情。这样做是语法错误。从某种意义上说,花括号{ ... } 是映射类型语法的一部分,尽管它们看起来 像其他对象类型中的花括号,但它们作用 em> 喜欢他们。

    重要的是不要将此语法与看起来相似的{[k: XXX]: YYY} 语法与index signatures 混淆。映射类型使用in 关键字,而索引签名不使用,并且需要一个虚拟键名称标识符(此处示例中的k)。虚拟键名只存在于键内部,不能在YYY表达式中使用,所以它不允许你在XXX中为不同的键分配不同的值;索引签名不会以任何方式迭代 XXX 内的键。作为签名,索引签名可以与其他属性一起用于任何对象类型,例如{[k: XXX]: YYY; somethingElse: string},只要其他属性不与索引签名冲突。它们可以包含在interface 声明中。花括号不是索引签名语法的一部分。


    因此,您的问题是:为什么您不能将其他属性添加到映射类型? 为什么映射类型中的花括号不能像在其他对象类型中那样工作?

    microsoft/TypeScript#13573 提出了这个确切的问题。不过,似乎没有一个确定的答案。这似乎是一个意料之外的用例。有一段时间,microsoft/TypeScript#26797 的拉取请求可能会被合并到语言中,作为统一映射类型和索引签名的努力的一部分,但这从未发生过。 TS 团队成员在microsoft/TypeScript#45089 中的relevant comment 解释说,目前这里不太可能发生任何变化,因为它提出了有关如何处理可能冲突的类型和generics 的问题:

    混合映射类型和属性声明会产生奇怪的影响,而这些可以通过交集优雅地解决

    我不会在这里明确地讨论这些;您可以查看链接的问题以获取更多信息。


    就目前而言,这就是“为什么会这样”的答案。那么你能做什么呢?上面的评论提到了intersections;您可以通过交集将两种对象类型的属性合并在一起,因此{a: string} & {b: string}{a: string; b: string} 基本相同。因此,编写对象类型的一种方法是:

    type MyObjType =
      { [K in Category]: string } &
      { customKey1: string, customKey2: string };
    

    您不能直接将交集用作接口类型,但您可以使用静态已知键创建接口extend 其他命名类型。您的 {[K in Category]: string} 具有静态已知键,因为 Category 不是通用的,但它不是命名类型。你可以自己命名,然后扩展它:

    type CategoryProps = { [K in Category]: string };
    interface MyObjType extends CategoryProps {
      customKey1: string
      customKey2: string
    }
    

    或者,您可以使用 the Record<K, V> utility type 并对其进行扩展,而无需声明新名称:

    interface MyObjType extends Record<Category, string> {
      customKey1: string
      customKey2: string
    }
    

    最后,您总是可以换个方向,为所有键使用映射类型,而不是尝试单独添加它们:

    type MyObjType = 
      { [K in Category | "customKey1" | "customKey2"]: string };
    

    Playground link to code

    【讨论】:

    • 非常感谢您的完整回答。我想您分享的链接将帮助我更好地理解 TS。阅读所说的我想我并没有真正区分映射类型和索引签名,我将深入处理这些概念。感谢您的解决方案,第一个是我在项目中使用的解决方案。我想我会使用带有Record 实用程序类型的第二个,比第三个更灵活(一些自定义键可能有不同的类型)。
    • 这是如何提供 SO 答案的终极指南。 WHY 的解释和来自 github 的链接缓解了一些挫败感。虽然我希望 TS Team 能够意识到这种新语法的好处。如果交集有效,那么很可能这种新语法可以编码为解释器技巧。再次感谢!
    【解决方案2】:

    对于那些不需要解释但只需要可用解决方案的人,这是我的:

    type Keys = { [key in Category]: string }
    interface Obj extends Keys {
      customKey1: string
      customKey2: string
    }
    const obj: Obj = {---}
    

    一切正常。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-11-19
      • 2023-03-26
      • 1970-01-01
      • 2022-12-07
      • 1970-01-01
      • 2019-02-16
      • 1970-01-01
      • 2010-12-10
      相关资源
      最近更新 更多