结构化类型系统#
class Cat {
meow() { }
eat() { } // 去掉 eat 不会报错
}
class Dog {
eat() { }
}
function feedCat(cat: Cat) { }
// 报错!
feedCat(new Dog())
这是因为,TypeScript 比较两个类型并非通过类型的名称(即 feedCat
函数只能通过 Cat 类型调用),而是比较这两个类型上实际拥有的属性与方法。也就是说,这里实际上是比较 Cat 类型上的属性是否都存在于 Dog 类型上。
在我们最初的例子里,Cat 与 Dog 类型上的方法是一致的,所以它们虽然是两个名字不同的类型,但仍然被视为结构一致,这就是结构化类型系统的特性。你可能听过结构类型的别称鸭子类型(Duck Typing),这个名字来源于鸭子测试(Duck Test)。其核心理念是,如果你看到一只鸟走起来像鸭子,游泳像鸭子,叫得也像鸭子,那么这只鸟就是鸭子。
class Cat {
eat() { }
}
class Dog {
bark() { }
eat() { }
}
function feedCat(cat: Cat) { }
feedCat(new Dog())
这个时候为什么却没有类型报错了?这是因为,结构化类型系统认为 Dog 类型完全实现了 Cat 类型。至于额外的方法 bark
,可以认为是 Dog 类型继承 Cat 类型后添加的新方法,即此时 Dog 类可以被认为是 Cat 类的子类。同样的,面向对象编程中的里氏替换原则也提到了鸭子测试:如果它看起来像鸭子,叫起来也像鸭子,但是却需要电池才能工作,那么你的抽象很可能出错了。
更进一步,在比较对象类型的属性时,同样会采用结构化类型系统进行判断。而对结构中的函数类型(即方法)进行比较时,同样存在类型的兼容性比较:
class Cat {
eat(): boolean {
return true
}
}
class Dog {
eat(): number {
return 599;
}
}
function feedCat(cat: Cat) { }
// 报错!
feedCat(new Dog())
标称类型系统#
标称类型系统(Nominal Typing System)要求,两个可兼容的类型,其名称必须是完全一致的,比如以下代码:
type USD = number;
type CNY = number;
const CNYCount: CNY = 200;
const USDCount: USD = 200;
function addCNY(source: CNY, input: CNY) {
return source + input;
}
addCNY(CNYCount, USDCount)
在标称类型系统中,CNY 与 USD 被认为是两个完全不同的类型,因此能够避免这一情况发生。在《编程与类型系统》一书中提到,类型的重要意义之一是限制了数据的可用操作与实际意义,这一点在标称类型系统中的体现要更加明显。比如,上面我们可以通过类型的结构,来让结构化类型系统认为两个类型具有父子类型关系,而对于标称类型系统,父子类型关系只能通过显式的继承来实现,称为标称子类型(Nominal Subtyping)。
在 TypeScript 中模拟标称类型系统#
export declare class TagProtector<T extends string> {
protected __tag__: T;
}
export type Nominal<T, U extends string> = T & TagProtector<U>;
在这里我们使用 TagProtector 声明了一个具有 protected
属性的类,使用它来携带额外的信息,并和原本的类型合并到一起,就得到了 Nominal 工具类型。
export type CNY = Nominal<number, 'CNY'>;
export type USD = Nominal<number, 'USD'>;
const CNYCount = 100 as CNY;
const USDCount = 100 as USD;
function addCNY(source: CNY, input: CNY) {
return (source + input) as CNY;
}
addCNY(CNYCount, CNYCount);
// 报错了!
addCNY(CNYCount, USDCount);
类型层级#
判断类型兼容性的方式#
对于变量 a = 变量 b,如果成立,意味着 <变量 b 的类型> extends <变量 a 的类型>
成立,即 b 类型是 a 类型的子类型
type Result = 'linbudu' extends string ? 1 : 2;
declare let source: string;
declare let anyType: any;
declare let neverType: never;
anyType = source;
// 不能将类型“string”分配给类型“never”。
neverType = source;
基础类型与字面量类型#
type Result1 = "linbudu" extends string ? 1 : 2; // 1
type Result2 = 1 extends number ? 1 : 2; // 1
type Result3 = true extends boolean ? 1 : 2; // 1
type Result4 = { name: string } extends object ? 1 : 2; // 1
type Result5 = { name: 'linbudu' } extends object ? 1 : 2; // 1
type Result6 = [] extends object ? 1 : 2; // 1
很明显,一个基础类型和它们对应的字面量类型必定存在父子类型关系。严格来说,object 出现在这里并不恰当,因为它实际上代表着所有非原始类型的类型,即数组、对象与函数类型,所以这里 Result6 成立的原因即是[]
这个字面量类型也可以被认为是 object 的字面量类型。我们将结论简记为,字面量类型 < 对应的原始类型。
联合类型#
type Result7 = 1 extends 1 | 2 | 3 ? 1 : 2; // 1
type Result8 = 'lin' extends 'lin' | 'bu' | 'du' ? 1 : 2; // 1
type Result9 = true extends true | false ? 1 : 2; // 1
type Result10 = string extends string | false | number ? 1 : 2; // 1
字面量类型 < 包含此字面量类型的联合类型,原始类型 < 包含此原始类型的联合类型
type Result11 = 'lin' | 'bu' | 'budu' extends string ? 1 : 2; // 1
type Result12 = {} | (() => void) | [] extends object ? 1 : 2; // 1
同一基础类型的字面量联合类型 < 此基础类型
合并一下结论,去掉比较特殊的情况,我们得到了这个最终结论:字面量类型 < 包含此字面量类型的联合类型(同一基础类型) < 对应的原始类型,即:
// 2
type Result13 = 'linbudu' extends 'linbudu' | '599'
? 'linbudu' | '599' extends string
? 2
: 1
: 0;
装箱类型#
type Result14 = string extends String ? 1 : 2; // 1
type Result15 = String extends {} ? 1 : 2; // 1
type Result16 = {} extends object ? 1 : 2; // 1
type Result18 = object extends Object ? 1 : 2; // 1
type Result16 = {} extends object ? 1 : 2; // 1
type Result18 = object extends {} ? 1 : 2; // 1
type Result17 = object extends Object ? 1 : 2; // 1
type Result20 = Object extends object ? 1 : 2; // 1
type Result19 = Object extends {} ? 1 : 2; // 1
type Result21 = {} extends Object ? 1 : 2; // 1
原始类型 < 原始类型对应的装箱类型 < Object 类型
Top Type#
type Result22 = Object extends any ? 1 : 2; // 1
type Result23 = Object extends unknown ? 1 : 2; // 1
type Result24 = any extends Object ? 1 : 2; // 1 | 2
type Result25 = unknown extends Object ? 1 : 2; // 2
type Result26 = any extends 'linbudu' ? 1 : 2; // 1 | 2
type Result27 = any extends string ? 1 : 2; // 1 | 2
type Result28 = any extends {} ? 1 : 2; // 1 | 2
type Result29 = any extends never ? 1 : 2; // 1 | 2
在 TypeScript 内部代码的条件类型处理中,如果接受判断的是 any,那么会直接返回条件类型结果组成的联合类型。
type Result31 = any extends unknown ? 1 : 2; // 1
type Result32 = unknown extends any ? 1 : 2; // 1
Object < any / unknown
Bottom Type#
type Result33 = never extends 'linbudu' ? 1 : 2; // 1
type Result34 = undefined extends 'linbudu' ? 1 : 2; // 2
type Result35 = null extends 'linbudu' ? 1 : 2; // 2
type Result36 = void extends 'linbudu' ? 1 : 2; // 2
never < 字面量类型
其他比较场景#
- 对于基类和派生类,通常情况下派生类会完全保留基类的结构,而不只是自己新增新的属性与方法。在结构化类型的比较下,其类型自然会存在子类型关系。更不用说派生类本身就是 extends 基类得到的。
- 对于联合类型地类型层级比较,我们只需要比较一个联合类型是否可被视为另一个联合类型的子集,即这个联合类型中所有成员在另一个联合类型中都能找到。
type Result36 = 1 | 2 | 3 extends 1 | 2 | 3 | 4 ? 1 : 2; // 1
type Result37 = 2 | 4 extends 1 | 2 | 3 | 4 ? 1 : 2; // 1
type Result38 = 1 | 2 | 5 extends 1 | 2 | 3 | 4 ? 1 : 2; // 2
type Result39 = 1 | 5 extends 1 | 2 | 3 | 4 ? 1 : 2; // 2
- 数组和元组
type Result40 = [number, number] extends number[] ? 1 : 2; // 1
type Result41 = [number, string] extends number[] ? 1 : 2; // 2
type Result42 = [number, string] extends (number | string)[] ? 1 : 2; // 1
type Result43 = [] extends number[] ? 1 : 2; // 1
type Result44 = [] extends unknown[] ? 1 : 2; // 1
type Result45 = number[] extends (number | string)[] ? 1 : 2; // 1
type Result46 = any[] extends number[] ? 1 : 2; // 1
type Result47 = unknown[] extends number[] ? 1 : 2; // 2
type Result48 = never[] extends number[] ? 1 : 2; // 1
References:
https://juejin.cn/book/7086408430491172901