TypeScript接口在分配对象时不强制执行属性

eivgtgni  于 2022-11-18  发布在  TypeScript
关注(0)|答案(3)|浏览(154)

我很难理解TypeScript的接口规则。我知道下面的代码块会抛出错误,因为id属性没有在接口中定义:

interface Person {
   name: string;
}

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

但是,如果我试图通过分配另一个对象来添加id属性,为什么TypeScript不会抛出错误呢?例如:

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

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

4dbbbstv

4dbbbstv1#

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

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

interface AgedPerson extends Person {
  age: number;
}

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

因为TypeScript有一个结构化类型系统,所以实际上不必为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赋给变量或将其作为参数传递时。
对象文字会得到特殊的处理,并且在第一次赋值给变量或作为函数参数传递时会进行额外的属性检查。如果对象文字具有未知的属性,而这些属性并不存在于预期的类型中,则会出现错误。如下所示:

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;
} */

并且没有多余的属性。samePersonperson的后续赋值也会成功,因为samePerson不是对象文字,因此多余的属性检查不适用。
Playground代码链接

p5fdfcr1

p5fdfcr12#

"让我们试着去理解"
接口是实体应该遵守的语法契约。
接口定义属性、方法和事件,它们是接口的成员。接口只包含成员的声明。定义成员是派生类的责任。它通常有助于提供派生类将遵循的标准结构
此外,我们可以实现多个接口,这样实现者就可以拥有一个接口定义的多个接口。
虽然类必须遵守契约中的内容,但它也可以有自己的附加实现(这在许多语言中都是如此)
来到 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

jogvjijk

jogvjijk3#

最后,在typescript 4.9中,使用satisfied运算符修复了这个问题,因此如果您像这样创建类型

type User = {
    name: string
    age: number
}

const user = { name: 'John Doe', age: 22 } satisfies User

它不允许在此对象中使用其他属性,如果您尝试添加它,则会给予错误

相关问题