【问题标题】:TypeScript interfaces not enforcing properties when an object is assignedTypeScript 接口在分配对象时不强制执行属性
【发布时间】:2022-04-08 10:51:47
【问题描述】:

我很难理解 TypeScript 的接口规则。我了解以下代码块会引发错误,因为接口中未定义 id 属性:

interface Person {
   name: string;
}

let person: Person = { name: 'Jack', id: 209 }

但是,如果我尝试通过分配另一个对象来添加 id 属性,为什么 TypeScript not 会抛出错误?比如这样:

const samePerson = { name: 'Jack', id: 209 };
person = samePerson;

person 对象最终会获得 id 属性,即使它没有在接口中定义。

【问题讨论】:

  • 因为 TS 实现了结构化类型类型系统:它允许这种类型的赋值。
  • 将接口视为最低要求,而不是最高要求

标签: typescript


【解决方案1】:

对象类型通常是开放和可扩展的,并且允许额外的属性...

TypeScript 中的对象类型通常是开放且可扩展的。一个对象是一个Person 当且仅当它有一个string 类型的name 属性。这样的对象可能会有额外的属性,但它仍然是Person。这非常有用,并且允许接口扩展形成类型层次结构:

interface AgedPerson extends Person {
  age: number;
}

const agedPerson: AgedPerson = { name: "Alice", age: 35 };
const stillAPerson: Person = agedPerson; // okay

而且因为 TypeScript 有一个structural type system,所以实际上你不必为AgedPerson 声明一个接口,它就可以被视为Person 的子类型:

const undeclaredButStillAgedPerson = { name: "Bob", age: 40 };
const andStillAPersonToo: Person = undeclaredButStillAgedPerson; // okay

这里undeclaredButStillAgedPerson 的类型为{name: string, age: number},相当于AgedPerson,随后分配给Person 的工作原理相同。


尽管开放/可扩展类型很有用,但它可能会造成混淆,有时甚至是不受欢迎的。 microsoft/TypeScript#12936 有一个长期开放的请求,要求 TypeScript 支持所谓的 exact 类型,其中像 Exact<Person> 这样的东西只允许拥有name财产,没有别的。 AgedPerson 将是 Person,但不是 Exact<Person>。目前没有直接支持此类确切类型。


...但是对象字面量确实会进行过多的属性检查。

跳回:TypeScript 中的对象类型通常是开放的。但是在一种情况下,对象将被视为其类型是精确的。这是当您将 object literal 分配给变量或将其作为参数传递时。

对象文字在首次分配给变量或作为函数参数传递时会得到特殊处理并经过excess property checking。如果对象字面量具有未知类型中不存在的属性,则会出现错误。像这样:

let person: Person = { name: 'Jack', id: 209 }; // error!
// ------------------------------->  ~~~~~~~
// Object literal may only specify known properties, 
// and 'id' does not exist in type 'Person'.

即使{name: "Jack", id: 209} 按照原始定义的Person,它不是Exact<Person>,所以我们得到一个错误。请注意,该错误特别提到了“对象文字”。


与以下对比,没有错误:

const samePerson = { name: 'Jack', id: 209 }; // okay
person = samePerson; // okay

将对象字面量分配给samePerson 没有错误,因为samePerson 的类型推断属于该类型

/* const samePerson: {
    name: string;
    id: number;
} */

那里没有多余的财产。后续将samePerson 赋值给person 也成功,因为samePerson 不是对象字面量,因此多余的属性检查不适用。


Playground link to code

【讨论】:

  • 但是如果{ name: 'Jack', id: 209 }被赋值给samePerson,那不就意味着这个变量包含了一个对象字面量吗?
  • 一个对象literalnotation,其中包含花括号和冒号等内容。一个对象字面量引用一个对象,但它不是对象本身。变量samePerson 包含对象字面量所引用的对象的引用,但它不包含该对象字面量(samePerson 任何地方都没有实际的花括号)。如果这太令人困惑,那么假设一个对象文字仅在“新鲜”时检查多余的属性,但一旦使用它就会失去新鲜度并且不被检查。
【解决方案2】:

让我们试着理解

接口是实体应遵守的语法契约。

接口定义属性、方法和事件,它们是接口的成员。接口只包含成员的声明。定义成员是派生类的责任。它通常有助于提供派生类遵循的标准结构

此外,我们可以实现多个接口,这样实现者就可以拥有多个接口定义的东西。

虽然类必须遵守合同中的规定,但它也可以有自己的附加实现(这在许多语言中大多如此)

来到 Typescript,就像 @zerkms 所说的

TypeScript 中的类型兼容性基于结构子类型。 结构类型是一种仅基于类型关联类型的方法 成员。

TypeScript 结构类型系统的基本规则是,如果 y 至少具有与 x 相同的成员,则 x 与 y 兼容。例如:

interface Pet {
  name: string;
}
let pet: Pet;
// dog's inferred type is { name: string; owner: string; }
let dog = { name: "Lassie", owner: "Rudd Weatherwax" };
pet = dog;

为了检查 dog 是否可以分配给 pet,编译器会检查 pet 的每个属性以在 dog 中找到对应的兼容属性。在这种情况下,dog 必须有一个名为 name 的成员,它是一个字符串。确实如此,因此允许分配。

参考:type-compatibility

【讨论】:

  • 这个答案解释了为什么const samePerson = { name: 'Jack', id: 209 }; person = samePerson; 成功,但没有解释为什么let person: Person = { name: 'Jack', id: 209 } 失败。
  • 我想我显然错过了解释那部分。不过你解释的很好。正如您提到的第一个示例是转换/分配,只要事情匹配它就会起作用。但是,在第二部分中,我们清楚地声明了 Person 类型的人并分配值会导致错误。
猜你喜欢
  • 2017-11-05
  • 2017-08-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-09
相关资源
最近更新 更多