TypeScript 类型系统

结构化类型系统

Copy
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）。其核心理念是，如果你看到一只鸟走起来像鸭子，游泳像鸭子，叫得也像鸭子，那么这只鸟就是鸭子。

Copy
class Cat {
eat() { }
}

class Dog {
bark() { }
eat() { }
}

function feedCat(cat: Cat) { }

feedCat(new Dog())

这个时候为什么却没有类型报错了？这是因为，结构化类型系统认为 Dog 类型完全实现了 Cat 类型。至于额外的方法 bark，可以认为是 Dog 类型继承 Cat 类型后添加的新方法，即此时 Dog 类可以被认为是 Cat 类的子类。同样的，面向对象编程中的里氏替换原则也提到了鸭子测试：如果它看起来像鸭子，叫起来也像鸭子，但是却需要电池才能工作，那么你的抽象很可能出错了。

更进一步，在比较对象类型的属性时，同样会采用结构化类型系统进行判断。而对结构中的函数类型（即方法）进行比较时，同样存在类型的兼容性比较：

Copy
class Cat {
eat(): boolean {
return true
}
}

class Dog {
eat(): number {
return 599;
}
}

function feedCat(cat: Cat) { }

// 报错！
feedCat(new Dog())

标称类型系统

标称类型系统（Nominal Typing System）要求，两个可兼容的类型，其名称必须是完全一致的，比如以下代码：

Copy
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 中模拟标称类型系统

Copy
export declare class TagProtector<T extends string> {
protected __tag__: T;
}

export type Nominal<T, U extends string> = T & TagProtector;

在这里我们使用 TagProtector 声明了一个具有 protected 属性的类，使用它来携带额外的信息，并和原本的类型合并到一起，就得到了 Nominal 工具类型。

Copy
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 类型的子类型

Copy
type Result = 'linbudu' extends string ? 1 : 2;

Copy
declare let source: string;

declare let anyType: any;
declare let neverType: never;

anyType = source;

// 不能将类型“string”分配给类型“never”。
neverType = source;

基础类型与字面量类型

Copy
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 的字面量类型。我们将结论简记为，字面量类型 < 对应的原始类型。

联合类型

Copy
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

Copy
type Result10 = string extends string | false | number ? 1 : 2; // 1

字面量类型 < 包含此字面量类型的联合类型，原始类型 < 包含此原始类型的联合类型

Copy
type Result11 = 'lin' | 'bu' | 'budu' extends string ? 1 : 2; // 1
type Result12 = {} | (() => void) | [] extends object ? 1 : 2; // 1

同一基础类型的字面量联合类型 < 此基础类型

合并一下结论，去掉比较特殊的情况，我们得到了这个最终结论：字面量类型 < 包含此字面量类型的联合类型（同一基础类型） < 对应的原始类型，即：

Copy
// 2
type Result13 = 'linbudu' extends 'linbudu' | '599'
? 'linbudu' | '599' extends string
? 2
: 1
: 0;

装箱类型

Copy
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

Copy
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

Copy
type Result22 = Object extends any ? 1 : 2; // 1
type Result23 = Object extends unknown ? 1 : 2; // 1

Copy
type Result24 = any extends Object ? 1 : 2; // 1 | 2
type Result25 = unknown extends Object ? 1 : 2; // 2

Copy
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，那么会直接返回条件类型结果组成的联合类型。

Copy
type Result31 = any extends unknown ? 1 : 2; // 1
type Result32 = unknown extends any ? 1 : 2; // 1

Object < any / unknown

Bottom Type

Copy
type Result33 = never extends 'linbudu' ? 1 : 2; // 1

Copy
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 基类得到的。

对于联合类型地类型层级比较，我们只需要比较一个联合类型是否可被视为另一个联合类型的子集，即这个联合类型中所有成员在另一个联合类型中都能找到。

Copy
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

数组和元组

Copy
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