跳至主要内容

子集与子类型

什么是子类型?

numberbooleanstring 这样的类型描述了一组可能的值。number 描述了所有可能的数字,因此单个数字(例如 42)将是 number 类型的子类型。相反,number 将是 42 类型的超类型

如果我们想知道一种类型是否为另一种类型的子类型,我们需要查看两种类型的所有可能值,并确定另一个类型是否具有这些值的子集

例如,如果我们有一个 TypeA,它描述了数字 1 到 3(联合 字面量类型),以及一个 TypeB,它描述了数字 1 到 5:TypeA 将被视为 TypeB子类型,因为 TypeATypeB 的子集。

1type TypeA = 1 | 2 | 3;2type TypeB = 1 | 2 | 3 | 4 | 5;

考虑一个 TypeLetters,它描述了字符串:"A"、"B"、"C",以及一个 TypeNumbers,它描述了数字:1、2、3。它们彼此都不是子类型,因为它们各自包含一组完全不同的值。

1type TypeLetters = "A" | "B" | "C";2type TypeNumbers =  1  |  2  |  3;

最后,如果我们有一个 TypeA,它描述了数字 1 到 3,以及一个 TypeB,它描述了数字 3 到 5。它们彼此都不是子类型。即使它们都包含 3 并描述了数字,但它们各自也有一些独特的项目。

1type TypeA = 1 | 2 | 3;2type TypeB = 3 | 4 | 5;

何时使用子类型?

Flow 所做的绝大多数工作是将类型彼此进行比较。

例如,为了知道您是否正确调用了函数,Flow 需要将您传递的参数与函数期望的参数进行比较。

这通常意味着确定您传递的值是否为预期值的子类型。

因此,如果您编写了一个期望数字 1 到 5 的函数,那么该集合的任何子类型都是可以接受的。

1function f(param: 1 | 2 | 3 | 4 | 5) {2  // ...3}4
5declare const oneOrTwo: 1 |  2; // Subset of the input parameters type.6declare const fiveOrSix: 5 | 6; // Not a subset of the input parameters type.7
8f(oneOrTwo); // Works!9f(fiveOrSix); // Error!
9:3-9:11: Cannot call `f` with `fiveOrSix` bound to `param` because number literal `6` [1] is incompatible with literal union [2]. [incompatible-call]

复杂类型的子类型

Flow 不仅需要比较基本值的集合,还需要能够比较对象、函数以及语言中出现的每种其他类型。

对象的子类型

您可以通过它们的键来开始比较两个对象。如果一个对象包含另一个对象的所有键,那么它可能是子类型。

例如,如果我们有一个 ObjectA,它包含键 foo,以及一个 ObjectB,它包含键 foobar。那么 ObjectB 可能就是 ObjectA 的子类型,如果 ObjectA 是不精确的。

1type ObjectA = {foo: string, ...};2type ObjectB = {foo: string, bar: number};3
4let objectB: ObjectB = {foo: 'test', bar: 42};5let objectA: ObjectA = objectB; // Works!

但我们也需要比较值的类型。如果两个对象都有一个键 foo,但一个是 number,另一个是 string,那么它们彼此就不是子类型。

1type ObjectA = {foo: string, ...};2type ObjectB = {foo: number, bar: number};3
4let objectB: ObjectB = { foo: 1, bar: 2 };5let objectA: ObjectA = objectB; // Error!
5:24-5:30: Cannot assign `objectB` to `objectA` because number [1] is incompatible with string [2] in property `foo`. [incompatible-type]

如果对象上的这些值恰好是其他对象,那么我们需要将它们彼此进行比较。我们需要递归地比较每个值,直到我们可以确定我们是否有一个子类型。

函数的子类型

函数的子类型规则更加复杂。到目前为止,我们已经看到,如果 B 包含 A 的所有可能值,那么 A 就是 B 的子类型。对于函数,这种关系如何应用尚不清楚。为了简化,您可以将函数类型 A 视为函数类型 B 的子类型,如果类型 A 的函数可以在期望类型 B 的函数的任何地方使用。

假设我们有一个函数类型和几个函数。哪些函数可以在期望给定函数类型的代码中安全使用?

1type FuncType = (1 | 2) => "A" | "B";2
3declare function f1(1 | 2): "A" | "B" | "C";4declare function f2(1 | null): "A" | "B";5declare function f3(1 | 2 | 3): "A";6
7f1 as FuncType; // Error
8f2 as FuncType; // Error
9f3 as FuncType; // Works!
7:1-7:2: Cannot cast `f1` to `FuncType` because string literal `C` [1] is incompatible with literal union [2] in the return value. [incompatible-cast]
8:1-8:2: Cannot cast `f2` to `FuncType` because literal union [1] is incompatible with number literal `2` [2] in the first parameter. [incompatible-cast]
  • f1 可以返回 FuncType 从未返回的值,因此依赖 FuncType 的代码在使用 f1 时可能不安全。它的类型不是 FuncType 的子类型。
  • f2 无法处理 FuncType 所做的一切参数值,因此依赖 FuncType 的代码无法安全使用 f2。它的类型也不是 FuncType 的子类型。
  • f3 可以接受 FuncType 所做的一切参数值,并且只返回 FuncType 所做的值,因此它的类型是 FuncType 的子类型。

一般来说,函数子类型规则是这样的:函数类型 B 是函数类型 A 的子类型,当且仅当 B 的输入是 A 的超集,并且 B 的输出是 A 的子集。子类型必须接受至少与父类型相同的输入,并且必须返回最多与父类型相同的输出。

在输入和输出上应用子类型规则的方向的决定由变异控制,这是下一节的主题。