首页 > 解决方案 > 打字稿中的传播运算符不保证正确的类型

问题描述

我正在使用扩展运算符创建具有更改字段的副本,如以下示例 ( run ) 所示。但是如果在创建对象字面量时更改了字段的类型,编译器不会抱怨:

class A{
    constructor(readonly a: number, readonly b: string[]){}
    copy(): A { // must return an object of type A
        return {
            ...this,
            b: 99 // should be of type string[]!
        }
    }
}

const a: A = new A(1, ["x", "y"])
const a1 = a.copy()
console.log(JSON.stringify(a1))

输出:

[LOG]: "{"a":1,"b":99}"

创建显式接口也无济于事:

interface IA {
    readonly a: number
    readonly b: string[]
}

...

copy(): IA { ...

看起来它与数组无关,因为下一个示例也会编译:

class A{
    constructor(readonly a: number, readonly b: number){}
    copy(): A {
        return {
            ...this,
            b: "my string" // should be of type number!
        }
    }
}

为什么这在 TypeScript 中是可能的?或者它是一个编译器错误?有没有办法避免

标签: typescriptspread-expression

解决方案


这是以下类型系统事实之间的交互:

  1. this确实是多态this类型。多态this是扩展当前类的类的隐式泛型类型参数。(参考见最后一段)
  2. 如果展开操作涉及泛型类型的操作数,则结果是展开的项目类型的交集。参考
  3. 一个交集可以分配给它的任何成分。因此,例如,该类型A & B可以分配给两者A,并且无论和(示例)B的任何属性之间是否存在任何不兼容性(参考参见分配兼容性)AB

在这里应用这 3 个规则,我们得到{ ...this, b: 99 }as this & { b: number }( proof ) 的类型。可以分配给Asince this extends A

例如,如果您键入 assert thisto A,您实际上会得到一个错误,因为不涉及通用操作数的扩展操作是正确键入的(ex

至于这是否是一个错误,我将其称为设计限制。很长一段时间以来,使用带有泛型类型操作数的扩展是不可能的。此功能是在 3.2 ( PR ) 中添加的,阅读 PR 中的注释我们可以清楚地看到团队意识到了这里的一些漏洞:

使用交集的替代方法是按照#10727探索{ ...T, ...U }的路线引入高阶类型运算符。虽然这起初看起来很有吸引力,但实现这个新的类型构造函数并赋予它我们已经为交集类型实现的所有功能需要大量的工作,而且对于大多数场景来说,它几乎不会或根本没有提高精度。特别是,仅当扩展表达式涉及具有不同类型的重叠属性名称的对象时,差异才真正重要。此外,对于不受约束的类型参数,该类型实际上不能分配给,这虽然在技术上是正确的,但会令人讨厌。T{ ...T, ...T }T

添加了上面的重点,您的用例属于这个确切的问题。


推荐阅读