TS:子类型关系

子类型关系

  • 1、概念
    • 1.1 里氏替换原则
    • 1.2 自反性
    • 1.3 传递性
  • 2、顶端类型 和 尾端类型
  • 3、字面量类型
  • 4、undefined 和 null
  • 5、枚举类型
  • 6、函数类型
    • 6.1 变型
      • 6.1.1 协变
      • 6.1.2 逆变
      • 6.1.3 双变
    • 6.2 函数类型间的子类型关系
      • 6.2.1 函数参数数量
      • 6.2.2 函数参数类型
        • A、非严格函数类型检查
        • B、严格函数类型检查
      • 6.2.3 函数返回值类型
      • 6.2.4 函数重载
  • 7、对象类型
    • 7.1 属性成员类型
    • 7.2 调用签名 和 构造签名
    • 7.3 字符串索引签名 和 数值型索引签名
  • 8、类类型
  • 9、泛型类型
    • 9.1 泛型对象类型
    • 9.2 泛型函数类型
      • 9.2.1 非严格泛型函数类型检查
      • 9.2.2 严格泛型函数类型检查
  • 10、联合类型 和 交叉类型
    • 10.1 联合类型
    • 10.2 交叉类型

1、概念

1.1 里氏替换原则

程序中任何使用了超类型 的地方都可以用其 子类型 进行替换,并且在替换后程序的行为保持不变。

1.2 自反性

任意类型都是其 自身 的子类型和超类型。

1.3 传递性

若 类型A 是 类型B 的子类型,且 类型B 是 类型C 的子类型,那么 类型A 也是 类型C 的子类型。

2、顶端类型 和 尾端类型

顶端类型是一种通用超类型,所有类型都是顶端类型的子类型;同时,尾端类型是所有类型的子类型。

TS 中存在两种顶端类型,即any类型和unknown类型。因此,所有类型都是any类型和unknown类型的子类型。

3、字面量类型

字面量类型是其对应的基础原始类型的子类型。

例如:maintainer 类型是 name 类型的子类型

type maintainer = 'Bob';
type name = 'string';

4、undefined 和 null

Undefined类型是 除尾端类型 never 外 所有类型的子类型,其中也包括null类型。
Null类型是 除尾端类型和undefined类型外 的所有类型的子类型。

5、枚举类型

在联合枚举类型中,每个枚举成员都能够表示一种类型,同时联合枚举成员类型是联合枚举类型的子类型。

什么是联合枚举类型?
枚举成员类型都是字面量类型的枚举类型。

6、函数类型

函数类型由 参数类型返回值类型 构成。在比较两个函数类型间的子类型关系时要同时考虑 参数类型返回值类型

6.1 变型

在学习函数类型的字类型和超类型之间的关系前,我们要先了解一个和函数类型关系非常紧密的概念——变型

所谓变型就是描述的是复杂类型的组成类型是如何影响复杂类型间的子类型关系的。

现约定如果复杂类型 Complex 是由 类型T 构成,那么我们将其记作 Complex(T)。

6.1.1 协变

假设有两个复杂类型 Complex(A) 和 Complex(B) ,如果由A是B的子类型能够得出 Complex(A) 是 Complex(B) 的子类型,那么我们将这种变型称作协变。

协变关系维持了复杂类型与其组成类型间的子类型关系。

6.1.2 逆变

如果由A是B的子类型能够得出 Complex(B) 是 Complex(A) 的子类型,那么我们将这种变型称作逆变。

6.1.3 双变

如果由 A 是 B 的子类型或者 B 是 A 的子类型能够得出 Complex(A) 是 Complex(B) 的子类型,那么我们将这种变型称作双变。

双变同时具有协变关系与逆变关系。

6.2 函数类型间的子类型关系

6.2.1 函数参数数量

子类型的 必选参数 的个数 不能多于 父类型 中的参数个数

若函数类型 S 是函数类型 T 的子类型,则 S 中的每一个必选参数必须能够在T中找到对应的参数。

当T中存在 可选参数剩余参数 时,函数类型检查是不可靠的。因为当使用子类型 S 替换了超类型 T 之后,调用 S 时的实际参数个数可能少于必选参数的个数。例如,有如下的函数s 和函数t ,其中 s 是 t 的子类型,使用一个实际参数调用函数 t 没有问题,但是将 t 替换为其子类型 s 后会产生错误,因为调用 s 需要两个实际参数。

function s(a: number, b: number): void {}
function t(...x:number[]): void {}

t(0);
s(0); // 编译错误

6.2.2 函数参数类型

函数的参数类型会影响函数类型间的子类型关系。

A、非严格函数类型检查

在该模式下,函数参数类型与函数类型是 双变 关系。

若函数类型 S 是函数类型 T 的子类型,那么 S 的参数类型必须是 T 中对应参数类型的【子类型】或者【超类型】。这意味着在对应位置上的两个参数只要存在子类型关系即可,而不强调哪一方应该是另一方的子类型。

type S = (a: 0 | 1) => void;
type T = (x: number) => void;

此例中,S 是 T 的子类型,同时 T 也是 S 的子类型。

B、严格函数类型检查

strictFunctionTypes 编译选项用来启用严格的函数类型检查。

在该模式下,函数参数类型与函数类型是 逆变 关系,而非相对宽松的双变关系。

若函数类型 S 是函数类型 T 的子类型,那么 S 的参数类型必须是 T 中对应参数类型的 超类型

6.2.3 函数返回值类型

在确定函数类型间的子类型关系时,编译器将检查函数返回值类型 是否兼容

不论是否启用了strictFunctionTypes 编译选项,函数返回值类型与函数类型始终是 协变 关系。

若函数类型 S 是函数类型 T 的子类型,那么 S 的返回值类型必须是 T 的返回值类型的 子类型

我理解,无论是函数返回值类型还是严格模式下的函数参数类型检查,都要满足赋值兼容性,才符合里氏替换原则。
对于严格模式下的参数类型,超类型函数的实参要能赋值给子类型的形参。
对于函数返回值类型,子类型函数的返回值要能赋值给超类型函数的返回值。

6.2.4 函数重载

在确定函数类型间的子类型关系时,编译器将检查函数重载签名类型是否兼容。

若函数类型 S 是函数类型 T 的子类型,并且 T 存在函数重载,那么 T 的每一个函数重载必须能够在 S 的函数重载中找到与其对应的子类型。

7、对象类型

在比较对象类型的子类型关系时要分别考虑 每一个 类型成员。

在结构化子类型系统中仅通过比较两个对象类型的 类型成员列表 就能够确定它们的子类型关系。对象类型的名称完全不影响对象类型间的子类型关系。

例如,以下示例中 类型 A 和 类型 B 是相同类型,类型 C 是 类型 A 和 类型 B 的超类型。

type A = {
	name: string,
	age: number
}
type B = {
	name: string,
	age: number
}
type C = {
	name: string
}

对象类型的子类型关系要满足以下条件:

  1. 数量上,超类型对象类型的成员数量不能多于 子类型对象类型的成员数量。
  2. 属性成员的可访问性上,超类型中的必选属性在子类型中也必须是必选属性。
  3. 成员类型上,子类型的成员类型是超类型中对应的成员类型的子类型。

7.1 属性成员类型

超类型的每个属性成员 M 都能够在子类型中找到同名属性成员 N,且 N 是 M 的子类型。

7.2 调用签名 和 构造签名

超类型中的调用签名 M 都能够在子类型中找到对应的调用签名 N,且 N 是 M 的子类型。

对象类型中的构造签名与调用签名有着相同的判断规则。

7.3 字符串索引签名 和 数值型索引签名

父类型中有索引签名,子类型中也要有,并子类型的索引签名类型是父类型索引签名类型的子类型。

8、类类型

在确定两个类类型之间的子类型关系时,仅检查类的 实例成员 类型,类的 静态成员类型 以及 构造函数类型 不进行检查。

如果类中存在 私有成员受保护成员,那么在确定类类型间的子类型关系时要求私有成员和受保护成员 来自同一个类,这意味着两个类需要存在 继承关系

例如,以下代码中,Point 和 Position 中的受保护成员 x 都来自Point,因此 Position 是 Point 的子类型。

class Point {
	protected x: number = 0;
}
class position extends Point {
	protected y: number = 0;
}

9、泛型类型

9.1 泛型对象类型

对于泛型接口、泛型类和表示对象类型的泛型类型别名而言,实例化泛型类型时使用的实际类型参数 不影响 子类型关系,真正影响子类型关系的是泛型实例化后的 结果对象类型

9.2 泛型函数类型

TS编译器提供了noStrictGenericChecks 编译选项用来启用或关闭严格泛型函数类型检查。

9.2.1 非严格泛型函数类型检查

编译器先将所有的泛型类型参数替换为 any 类型,然后再确定子类型关系。这意味着泛型类型参数不影响泛型函数的子类型关系。

type A = <T, U>(x: T, y: U) => [T, U];
type B = <S>(x: S, y: S) => [S, S];

将所有的类型参数替换为any类型,结果如下:

type A = (x: any, y: any) => [any, any];
type B = (x: any, y: any) => [any, any];

9.2.2 严格泛型函数类型检查

先通过类型推断来 统一 两个泛型函数的类型参数,然后再确定两者的子类型关系。

type A = <T, U>(x: T, y: U) => [T, U];
type B = <S>(x: S, y: S) => [S, S];

如果我们想要确定 A 是否为 B 的子类型,那么先尝试使用 B 的类型来推断A的类型。通过比较每个参数类型和返回值类型,能够得出类型参数 T 和 U 均为 S。接下来使用推断的结果来实例化 A 类型,即将类型 A 中的 T 和 U 均替换为 S,替换后的结果如下:

type A = <S>(x: S, y: S) => [S, S];
type B = <S>(x: S, y: S) => [S, S];

在统一了类型参数之后,再来比较泛型函数间的子类型关系。因为统一后的类型 A 和 B 相同,所以 A 是 B 的子类型。

如果我们最开始想要确定 B 是否为 A 的子类型,那么这时将由 A 向 B 来推断并统一类型参数的值。经推断,S 的类型为联合类型“T | U”,然后使用“S = T | U”来实例化 B 类型,结果如下:

type A = <T, U>(x: T, y: U) => [T, U];
type B = <T, U>(x: T | U, y: T | U) => [T | U, T | U];

此时,B 不是 A 的子类型,因为 B 的返回值类型不是 A 的返回值类型的子类型。

10、联合类型 和 交叉类型

10.1 联合类型

联合类型由若干成员类型构成,在计算联合类型的子类型关系时需要考虑每一个成员类型。

假设有联合类型 S = S0 | S1 和任意类型 T,如果成员类型 S0 是类型 T 的子类型,并且成员类型 S1 是类型 T 的子类型,那么联合类型 S 是类型 T 的子类型。例如,有如下定义的联合类型 S 和类型 T 。

type S = 0 | 1;
type T = number;

此例中,联合类型 S 是类型 T 的子类型。

假设有联合类型 S = S0 | S1 和任意类型 T,如果类型 T 是成员类型 S0 的子类型,或者类型T是成员类型 S1 的子类型,那么类型 T 是联合类型 S 的子类型。例如,有如下定义的联合类型 S 和类型 T :

type S = number | string;
type T = 0;

此例中,类型T是联合类型S的子类型。

10.2 交叉类型

交叉类型由若干成员类型构成,在计算交叉类型的子类型关系时需要考虑每一个成员类型。

假设有交叉类型 S = S0 & S1 和任意类型 T,如果成员类型 S0 是类型 T 的子类型,或者成员类型 S1 是类型 T 的子类型,那么交叉类型 S 是类型 T 的子类型。

type S = { x: number } & { y: number };
type T = { x: number };

假设有交叉类型 S = S0 & S1 和任意类型 T,如果类型 T 是成员类型 S0 的子类型,并且类型 T 是成员类型 S1 的子类型,那么类型 T 是交叉类型 S 的子类型。

type S = { x: number } & { y: number };
type T = { x: number; y: number; z: number };

你可能感兴趣的:(TypeScript,typescript,前端)