详解 TypeScript 中奇怪的赋值操作
本文深入探讨 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 赋值操作的内容,请搜索本站以前的文章或继续浏览下面的相关文章。希望大家以后多多支持本站!
JavaScript闭包与类(Class)的区别及适用场景对比
2026-11-05 15:48:57
JavaScript入门要学ES6吗?let与箭头函数等新特性解析
2026-11-05 14:49:16
JavaScript 8种数据类型详解:基本类型与引用类型的区别
2026-11-05 14:25:15
怎么在JavaScript中使用async/await优化Ajax异步流程
2026-11-05 13:56:15
JavaScript replace回调函数实现动态正则替换逻辑
2026-11-05 13:52:13








