编程与类型系统07
发表于:2024-07-02 | 分类: 学习

子类型

子类型:如果在期望类型 T 的实例的任何地方,都可以安全地使用类型 S 的实例,那么称类型 S 是类型 T 的子类型。

名义和结构子类型:在名义子类型中,如果显式声明一个类型是另一个类型的子类型,则二者构成子类型关系。在结构子类型中,如果一个类型具有另一个类型的所有成员,并且可能还有其他成员,那么前者是后者的子类型。TypeScript 是结构子类型

在 TypeScript 中模拟名义子类型

为了实现这一点,我们可以添加一个唯一类型成员。在 TypeScript 中,unique symbol 生成了一个在所有代码中保持唯一的“名称”。不同的 unique symbol 声明将生成不同的名称,用户声明的名称绝不会匹配生成的名称。

1
declare const OnlyType: unique symbol

现在有了唯一名,我们可以将这个名称放到方括号内,从而使用该名称创建一个属性。我们需要为这个属性定义一个类型,但并不真的要给它提供有意义的值,因为使用它只是为了区分类型。我们不关心它的实际值,所以在这里最适合使用单元类型。因此,我们使用了 void。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
declare const OneType: unique symbol;
class One {
value: string;
[OneType]: void;
constructor(value: string) {
this.value = value;
}
}

declare const TwoType: unique symbol;
class Two {
value: string;
[TwoType]: void;
constructor(value: string) {
this.value = value;
}
}
function greet(name: One): void {
console.log(`Hi ${name.value}`);
}
greet(new One("lily"));

TypeScript 提供了is来判断某个值是否是给定的类型

1
2
3
4
5
6
7
8
9
10
11
12
class User {
name: string;
constructor(name: string) {
this.name = name;
}
}
function isUser(user: any): user is User {
if (user === null || user === undefined) {
return false;
}
return typeof user.name === "string";
}

顶层类型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
    25
    class 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>具有不变性,因为它不考虑子类型关系“TriangleShape的子类型”,所以List<Shape>List<Triangle>之间不存在子类型–父类型关系。

  • 逆变:如果一个类型颠倒了其底层类型的子类型关系,则称该类型具有逆变性。在大部分编程语言中,函数的实参是逆变的。一个接受 Triangle 作为实参的函数可以被替换为一个接受 Shape 作为实参的函数。函数之间的关系与其实参类型之间的关系相反。如果 Triangle 是 Shape 的子类型,那么接受 Triangle 作为实参的函数的类型是接受 Shape 作为实参的函数的类型的父类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    declare 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互为子类型

上一篇:
编程与类型系统08
下一篇:
编程与类型系统06