详解 TypeScript 中奇怪的赋值操作

作者:半夏

时间:2026-11-01

浏览:118

本文深入探讨 TypeScript 中看似矛盾的赋值行为,揭示其背后的结构化类型系统原理。文章解释了为何多出属性的对象字面量赋值会报错,而变量赋值却通过检查,重点解析严格属性检查(ECP)与类型兼容性的区别,帮助开发者理解 TypeScript 作为结构化系统的核心机制。

文章目录

前言

前言

本文主要探讨 TypeScript 中一些看似异常的赋值语句,旨在剖析其背后的逻辑,从而更深入地理解 TypeScript 作为结构化类型系统的核心特性。

结构化类型系统基础

首先观察以下示例:

// 定义一个Cat类
class Cat {
  name!: string
  age!: number
​
  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }
}
// 定义一个Dog接口类型
interface Dog {
  name: string
  age: number
}
​
let dog: Dog = {
  name: 'wuyue',
  age: 3
}
​
let cat: Cat
cat = new Cat('qiqi', 2)
cat = dog // is ok 
dog = cat // is ok 
cat = { name: 'jack', age: 12 } // is ok
  • 在上述代码中,我们定义了一个 Cat 类和一个 Dog 接口。Cat 类的实例与 Dog 类型的变量之间可以相互赋值,并通过 TypeScript 的类型检查;同时,将 { name: 'jack', age: 12 } 赋值给 Cat 类型的变量也是合法的。
  • 这体现了结构化类型系统的特性,TypeScript 正是基于这种结构类型构建的。结构化类型系统的核心理念是:名称并不重要,关键在于对象是否拥有相同的成员结构。如果结构一致,则视为兼容。这种机制也被称为“鸭子类型”:如果一只动物走起来像鸭子、游起来像鸭子、叫起来也像鸭子,那么它就可以被当作鸭子处理。
  • 在前面的例子中,cat 变量、dog 变量以及 { name: 'jack', age: 12 } 字面量都拥有相同的结构(name 和 age),因此 TypeScript 在类型检查时判定它们之间的赋值操作是合法的。

对象字面量的严格属性检查

接下来看另一个示例:

// 定义一个Cat类
class Cat {
  name!: string
  age!: number
​
  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }
}
// 定义一个Dog接口类型
interface Dog {
  name: string
  age: number
}
let dog: Dog = {
  name: 'wuyue',
  age: 3
}
​
function getCat(cat: Cat) {}
​
getCat(new Cat('cc', 2)) // ok
getCat(dog) // ok
getCat({ name: 'jack', age: 12 }) // ok
const obj = { name: 'jack', age: 12, sex: 1 } // ok
getCat(obj) // ? ok

在此示例中,Cat 类和 Dog 接口的定义与前述一致。随后定义了一个名为 getCat 的函数,该函数接收一个 Cat 类型的参数。

基于对结构化类型系统的理解,我们可以迅速判断 new Cat('cc', 2)、dog 以及 { name: 'jack', age: 12 } 都可以赋值给 Cat 类型的变量。那么最后一个情况呢?我们在 TypeScript Playground 中测试发现,代码通过了类型检查。然而,你可能会感到困惑:obj 对象明显多出了一个 sex 属性,而 Cat 类型定义中并不包含该属性,TypeScript 为何仍通过检查?暂且放下这个疑问,我们先看下面这行代码:

getCat({ name: 'jack', age: 12, sex: 1 }) 

前言

这行代码竟然报错了,情况变得更加复杂。

变量赋值与类型兼容性

再看一个示例:

前言

这看起来非常奇怪。同样的值,直接赋值或作为参数传递时会报错,但如果先定义一个变量,再将该变量进行赋值或传递,则不会报错。

查阅官方文档及相关资料后,可以得出以下结论:在 TypeScript 的类型系统中,对象的定义方式会影响类型检查的结果。当创建一个对象字面量并直接赋值给具有特定类型的变量时,TypeScript 会执行最严格的验证,即进行严格属性检查(ECP);而当将对象变量赋值给另一个变量时,TypeScript 会进行兼容性判断,如果结构兼容则通过检查,否则报错。

那么,这种兼容性判断是如何进行的呢?请记住以下原则:

在 TypeScript 中,如果要将 x 赋值给 y,x 必须比 y 更具体,即 x 必须包含 y 的所有属性,或者拥有更多属性。这种可分配性本质上就是类型兼容性。

例如:let v = { name: 'john'; age: 20 } 兼容 { name: string },类型兼容,检查通过。let v = {foo: 1, bar: 2} 兼容 {foo: number},类型兼容,检查通过。这种现象有时被称为“类型加宽”。

对于上述现象,既有优势也有劣势。有人因此提出 TypeScript 应引入精确类型语法,这一提议曾引发激烈讨论,详见相关议题 #12936。

总结与思考

结尾

在网络上的大多数案例中,经常会出现如下示例:

interface Person {
  name: string
  age: number
}

let p: Person = {
  name: 'jack',
  age: 12,
}

我们往往认为 p 变量必须精确满足 Person 接口的定义,如果多出属性就会报错。这似乎是 TypeScript 的基石,即 TypeScript 应该这样做。但实际上并非如此。TypeScript 作为一个结构化系统,多出属性并不会导致报错。“多出属性即报错”的功能实际上是 TypeScript 中一种 Linter(代码检查工具)的校验行为,且仅适用于对象字面量。因此,有时我们也会说 TypeScript 是一个功能强大的 JavaScript Linter 工具。

到此,关于详解 TypeScript 中奇怪赋值操作的文章介绍完毕。更多关于 TypeScript 赋值操作的内容,请搜索本站以前的文章或继续浏览下面的相关文章。希望大家以后多多支持本站!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章