子类型
子类型:如果在期望类型 T 的实例的任何地方,都可以安全地使用类型 S 的实例,那么称类型 S 是类型 T 的子类型。
名义和结构子类型:在名义子类型中,如果显式声明一个类型是另一个类型的子类型,则二者构成子类型关系。在结构子类型中,如果一个类型具有另一个类型的所有成员,并且可能还有其他成员,那么前者是后者的子类型。TypeScript 是结构子类型
在 TypeScript 中模拟名义子类型
为了实现这一点,我们可以添加一个唯一类型成员。在 TypeScript 中,unique symbol 生成了一个在所有代码中保持唯一的“名称”。不同的 unique symbol 声明将生成不同的名称,用户声明的名称绝不会匹配生成的名称。
1 | declare const OnlyType: unique symbol |
现在有了唯一名,我们可以将这个名称放到方括号内,从而使用该名称创建一个属性。我们需要为这个属性定义一个类型,但并不真的要给它提供有意义的值,因为使用它只是为了区分类型。我们不关心它的实际值,所以在这里最适合使用单元类型。因此,我们使用了 void。
1 | declare const OneType: unique symbol; |
TypeScript 提供了is来判断某个值是否是给定的类型
1 | class User { |
顶层类型unknown:如果我们能够把任何值赋给一个类型,就称该类型为顶层类型,因为其他任何类型都是该类型的子类型。换句话说,该类型位于子类型层次结构的顶端
unknown 和 any 的区别:尽管我们可以把任意值赋值给 unknown 和 any,但是在使用这两种类型的变量时,存在一个区别。对于 unknown 的情况,只有当我们确认一个值具有某个类型(如 User)时,才能把该值用作该类型(例如前面把 user 返回为 User 的函数中那样)。对于 any 的情况,我们可以立即把该值用作其他任何类型的值。any 会绕过类型检查。
底层类型never:如果一个类型是其他类型的子类型,那么我们称之为底层类型,因为它位于子类型层次结构的底端。要成为其他类型的子类型,它必须具有其他类型的成员。因为我们可以有无限个类型和成员,所以底层类型也必须有无限个成员。这是不可能发生的,所以底层类型始终是一个空类型:这是我们不能为其创建实际值的类型
由于 never 是底层类型,所以我们能把它赋值给其他任何类型,这也意味着我们能够从函数中返回该类型。由于这是向上转换(将某个值从子类型转换为父类型),可被隐式完成,因此编译器不会报错。
允许的替换
协变:如果一个类型保留其底层类型的子类型关系,就称该类型具有协变性。数组具有协变性,因为它保留了子类型关系:Triangle 是 Shape 的子类型,所以 Triangle[]是 Shape[]的子类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25class Shape {}
declare const TriangleType: unique symbol;
class Triangle extends Shape {
[TriangleType]: void;
}
class LinkedList<T> {
value: T;
next: LinkedList<T> | undefined = undefined;
constructor(value: T) {
this.value = value;
}
append(value: T): LinkedList<T> {
this.next = new LinkedList(value);
return this.next;
}
}
declare function makeTriangle(): LinkedList<Triangle>;
declare function makeShape(): LinkedList<Shape>;
declare function draw(shape: LinkedList<Shape>): void;
draw(makeTriangle());不变性:如果一个类型不考虑其底层类型的子类型关系,就称该类型具有不变性。C#中的
List<T>具有不变性,因为它不考虑子类型关系“Triangle是Shape的子类型”,所以List<Shape>和List<Triangle>之间不存在子类型–父类型关系。逆变:如果一个类型颠倒了其底层类型的子类型关系,则称该类型具有逆变性。在大部分编程语言中,函数的实参是逆变的。一个接受 Triangle 作为实参的函数可以被替换为一个接受 Shape 作为实参的函数。函数之间的关系与其实参类型之间的关系相反。如果 Triangle 是 Shape 的子类型,那么接受 Triangle 作为实参的函数的类型是接受 Shape 作为实参的函数的类型的父类型
1
2
3
4
5
6
7
8
9
10declare function drawShape(shape: Shape): void;
declare function drawTriangle(triangle: Triangle): void;
function render(
triangle: Triangle,
drawFunc: (argument: Triangle) => void
): void {
drawFunc(triangle);
}
render(new Triangle(),drawShape)双变性:如果类型的底层类型的子类型关系决定了它们互为子类型,则称这种类型具有双变性。在 TypeScript 中,如果 Triangle 是 Shape 的子 类 型 , 那 么 函 数 类 型
(argument: Shape) => void和(argument:Triangle) => void互为子类型