TypeScript 背后的结构化类型系统原理详解
本文深入解析 TypeScript 的结构化类型系统,对比其与标称类型系统及鸭子类型的区别,并详细演示如何通过交叉类型和类在 TypeScript 中模拟标称类型系统,以解决类型别名导致的兼容性问题。
前言
在深入探讨 TypeScript 的结构化类型系统之前,有必要厘清类型、类型系统与类型检查这三个核心概念及其相互关系。
- 类型是对变量访问权限和赋值行为的约束。例如,TypeScript 包含原始类型、对象类型、函数类型及字面量类型等。一旦变量被赋予特定类型,便无法访问该类型未定义的属性或方法,也不能将不兼容类型的变量赋值给它。
- 类型系统是一组规则集合,用于描述如何为变量、函数等结构分配并实施类型。它同时也定义了判断类型间兼容性的机制,这正是本文重点讨论的内容。
- 类型检查是一种确保类型遵循类型系统所定义兼容性的能力。
理解上述概念有助于深入掌握 TypeScript 的结构化类型系统。
类型系统主要分为结构化类型系统和标称类型系统,接下来我们将分别介绍这两者的定义。
什么是结构化类型系统?
结构化类型系统基于类型结构进行兼容性判断。
其核心在于两个类型的比较:若比较依据是属性和方法是否一致,则称为结构化类型系统,也被称为鸭子类型。
以下示例展示了这一机制:
class Dog {
say() {
console.log('wang wang wang!')
}
}
class Cat {
say() {
console.log('miao miao miao!')
}
}
const invokeSay = (dog: Dog) => {
dog.say()
}
const dog = new Dog()
const cat = new Cat()
invokeSay(dog) // wang wang wang!
invokeSay(cat) // miao miao miao!
尽管 `invokeSay` 函数接收的参数类型定义为 `Dog`,但由于 `Cat` 类型拥有与 `Dog` 相同的结构(即都包含一个 `say` 方法),因此 `Cat` 被视为兼容类型。这体现了结构化类型系统基于结构进行兼容性判断的特点。
代表语言包括:C#、Python、Objective-C。
什么是标称类型系统?
标称类型系统基于类型名称进行兼容性判断。
与结构化类型系统相对,标称类型系统在判断两个类型是否相同时,仅依据名称是否一致。即使内部结构完全相同,若名称不同,也不视为同一类型。示例如下:
/** @description 人民币 */
type CNY = number
/** @description 美元 */
type USD = number
const CNYCount: CNY = 666
const USDCount: USD = 333
const addCount = (source: CNY, input: CNY) => source + input
addCount(CNYCount, USDCount)
在标称类型系统中,调用 `addCount` 会报错。尽管 `CNY` 和 `USD` 本质上都是 `number` 类型,但由于名称不同,系统将其视为不同类型。
代表语言包括:C++、Java、Rust。
结构化类型系统等价于鸭子类型系统吗?
严格来说,两者并不等同。
- 结构化类型系统依据完整的类型结构来判断兼容性。
- 鸭子类型系统则基于运行时实际访问的属性或方法来判断兼容性。
由于 TypeScript 的类型检查发生在编译时而非运行时,因此它并不严格等价于鸭子类型系统。
如何在 TypeScript 中模拟标称类型系统?
由于 TypeScript 采用结构化类型系统,前文提到的 `CNY` 和 `USD` 示例在 TypeScript 中会正常运行。
/** @description 人民币 */
type CNY = number
/** @description 美元 */
type USD = number
const CNYCount: CNY = 666
const USDCount: USD = 333
const addCount = (source: CNY, input: CNY) => source + input
addCount(CNYCount, USDCount)
我们的意图是让 `CNY`(人民币)和 `USD`(美元)成为两种不同的类型。然而,受限于 TypeScript 的结构化特性,两者均被视为 `number` 类型,从而被判定为同一类型。
在标称类型系统中不会出现此问题。若能在 TypeScript 中模拟实现标称类型系统,即可符合预期行为,具体方法如下。
对比两种系统的特点,若在两个完全兼容的结构化类型中引入一个标识符,即使结构兼容,因标识符不同,系统也会将其视为不同类型。
利用这一特性,我们可以在 TypeScript 中模拟标称类型系统。
以下是两种模拟实现方式:
- 通过交叉类型实现——仅在类型层面有效,无法在运行时逻辑中体现。
- 通过类实现——可同时兼顾类型层面和运行时逻辑层面。
交叉类型实现
我们可以定义一个工具类型 `Nominal`,对传入的泛型参数进行处理,将其与一个特殊类型进行交叉操作。该特殊类型提供的标识符即为前文提到的标记。
nominal.ts
class TagProtector<T extends string> {
protected __tag__: T
}
export type Nominal<T, U extends string> = T & TagProtector<U>
index.ts
import { Nominal } from './nominal'
/** @description 人民币 */
type CNY = Nominal<number, 'CNY'>
/** @description 美元 */
type USD = Nominal<number, 'USD'>
const CNYCount = 666 as CNY
const USDCount = 333 as USD
const addCount = (source: CNY, input: CNY) => source + input
// 类型“USD”的参数不能赋给类型“CNY”的参数。
// 不能将类型“USD”分配给类型“TagProtector<"CNY">”。
// 属性“__tag__”的类型不兼容。
// 不能将类型“"USD"”分配给类型“"CNY"”。ts(2345)
addCount(CNYCount, USDCount)
由于 TypeScript 在运行时仍基于 JavaScript 执行,类型代码会被擦除。擦除类型后,代码仍可正常执行。
这表明通过交叉类型模拟标称类型系统仅能在类型层面生效,运行时无法进行实际检查。因此,需采用下文方案——使用类来模拟。
类实现
/** @description 人民币 */
class CNY {
private __tag__!: void
constructor(public value: number) {}
}
/** @description 美元 */
class USD {
private __tag__!: void
constructor(public value: number) {}
}
const CNYCount = new CNY(666)
const USDCount = new USD(666)
const addCount = (source: CNY, input: CNY) => source.value + input.value
// 类型“USD”的参数不能赋给类型“CNY”的参数。
// 类型具有私有属性“__tag__”的单独声明。
addCount(CNYCount, USDCount)
上述两种方案的本质均是通过一个非公开的额外属性为类型添加标识符,从而使结构化类型系统能将其判断为不同的类型。
总结
相信通过本文介绍,您已理解结构化类型系统的概念。如前所述,它定义了判断类型兼容性的规则。
结构化类型系统依据类型结构判断兼容性:只要两个类型的结构兼容(即一个类型的所有属性和方法均存在于另一个类型中),即可视为兼容。
此外,我们还了解了与结构化类型系统对应的标称类型系统,并掌握了在 TypeScript 中模拟实现标称类型系统的方法,从而对结构化类型系统有了更深刻的理解。
以上就是 TypeScript 背后的结构化类型系统原理详解的详细内容,更多关于 TypeScript 结构化类型系统的资料请关注本站其它相关文章!
D3.js 比例尺与坐标轴的配置详解
2026-11-05 16:54:46
JavaScript怎么遍历对象的Symbol键和不可枚举属性
2026-11-05 13:13:39
封装一个健壮的JS字符串判空工具函数
2026-11-05 12:16:14
JavaScript合并数组的三种常用方法:concat、扩展运算符与push
2026-11-05 11:22:29
JavaScript立即执行函数(IIFE)是什么及基本语法
2026-11-05 10:51:19








