Appearance
类型理解再升级-型变
我们先来看这个例子:
typescript
type User = {
id?: number;
name: string;
};
type Animal = {
id?: number;
name: string;
};
type AdminUser = {
id?: number;
name: string;
role: string;
};
function deleteUser(user: User) {
console.log(user);
}
const a1: Animal = {
id: 1,
name: 'animal1',
};
const u1: AdminUser = {
id: 2,
name: 'user2',
role: 'admin',
};
deleteUser(a1); // OK? Error?
deleteUser(u1); // OK? Error?答案:
deleteUser(a1); 正确
deleteUser(u1); 正确
结构化类型系统
TypeScript 的类型系统特性:结构化类型系统。TypeScript 比较两个类型并非通过类型的名称,而是比较这两个类型上实际拥有的属性与方法。User 与 Animal 类型上是一致的,所以它们虽然是两个名字不同的类型,但仍然被视为结构一致,这就是结构化类型系统的特性。你可能听过结构类型的别称鸭子类型(Duck Typing),这个名字来源于鸭子测试(Duck Test)。其核心理念是,如果你看到一只鸟走起来像鸭子,游泳像鸭子,叫得也像鸭子,那么这只鸟就是鸭子。
因此:deleteUser(a1);正确
deleteUser(u1);为什么也是正确的?
在很多类型系统中,都有子类型与超(父)类型的概念。当然了在java,c#这种后端名义型类型系统中子类型和父类型很容易区分,他们必须要extends,implements关键字。
但是在TS中,是通过结构进行区分的,不一定强制需要extends,implements关键字标注父子关系。比如上面的User和AdminUser。
明明u1多了一个属性role,这是因为,结构化类型系统认为 AdminUser 类型完全实现了 User 类型。至于额外的属性 role,可以认为是 AdminUser 类型继承 User 类型后添加的新属性,即此时 AdminUser 类型可以被认为是 User 类型的子类型。
协变
在很多类型系统中,都有型变的概念,也就是类型变化的意思,在型变的系统中,
子类型可以赋值给父类型,叫做协变
父类型可以赋值给子类型,叫做逆变
之前的基础类型,我们一直在强调类型兼容性的问题,不同的类型当然没有兼容性可言,要谈兼容性,至少需要父子关系。至于所谓父子关系的兼容性,一般都具有下面的含义:
给定两个类型A和B,假设B是A的子类型,那么在需要A的地方都可以放心使用B

从上图中可以看出:
- Array是Object的子类型,需要Object的地方都可以使用Array
- Tuple是Array的子类型,需要Array的地方都可以使用Tuple
- 所有类型都是any的子类型,需要any的地方,任何类型都能用
- never是所有类型的子类型。
- 字面量类型是对应基础类型的子类型,需要基础类型的地方都能使用字面量类型
对于结构化类型,主要的型变方式就是协变。因此,
AdminUser是User的子类型,那么需要User的地方,就都可以使用AdminUser
deleteUser(u1);是正确的,不会报错。
不过这仅仅是协变的基础形态,因为对于结构比较复杂对象来说,每一个具体的属性,都有可能还是比较复杂的形态。
typescript
type ExistUser = {
id: number;
name: string;
};
type LegacyUser = {
id?: number | string;
name: string;
};
const u2: ExistUser = {
id: 1,
name: 'user1',
};
const u3: LegacyUser = {
id: 3,
name: 'user3',
};
deleteUser(u2); // OK? Error?
deleteUser(u3); // OK? Error?新加了两种类型,注意和之前User类型的区别主要在id这个属性上
typescript
User ---> id ---> number | undefined
ExistUser ---> id ---> number
LegacyUser ---> id ---> number | string | undefined也就是说,每个类型的id属性的类型是不一样的,这里是联合类型,联合类型也有子类型和父类型的兼容关系。联合类型的父子关系的区分和基础类型是一样的。简单来说,越具体的,越形象化的,就是子类型
"hello" 字面量类型 比 string类型 更具体,那么"hello"字面量类型就是string类型的子类型
[number, number]元组类型比数组类型更具体,那么元组类型就是数组类型的子类型
a | b联合类型 比a | b | c联合类型更具体,那么a | b就是a | b | c的子类型当然,如果你不能理解上面为啥
a | b就是a | b | c的子类型,你可以这么想, 你妈你去市场买水果,买梨子 | 苹果肯定比买梨子 | 苹果 | 西瓜更具体
因此,就id这一个属性来说:
typescript
ExistUser < User < LegacyUser;由于另外一个属性是一样的,所以:
typescript
ExistUser < User < LegacyUser;那么我们就可以得出结论
typescript
deleteUser(u2); 正确
deleteUser(u3);
错误 `id`类型不兼容,不能将`number | string | undefined`赋值给`number | undefined`
不能把父类型赋值给子类型typescript对于结构(对象和类)的属性类型进行了协变,也就是说,如果想保证A对象可赋值给B对象,那么A对象的每个属性都必须是B对象对应属性的子类型。
如果A是B的子类型,那么我们可以说由A组成的复合类型(例如数组和泛型)也是B组成相应复合类型的子类型
typescript
type Pet = {
name: string;
};
type Dog = Pet & {
breed: string;
};
const dogs: Dog[] = [
{
name: 'Max',
breed: 'Labrador',
},
{
name: 'Rusty',
breed: 'Dalmatian',
},
];
const pets: Pet[] = dogs;
type Arrs<T> = {
arr: T[];
};
const arrs1: Arrs<Dog> = {
arr: dogs,
};
const arrs2: Arrs<Pet> = arrs1;