子集与子类型
什么是子类型?
像 number
、boolean
或 string
这样的类型描述了一组可能的值。number
描述了所有可能的数字,因此单个数字(例如 42
)将是 number
类型的子类型。相反,number
将是 42
类型的超类型。
如果我们想知道一种类型是否为另一种类型的子类型,我们需要查看两种类型的所有可能值,并确定另一个类型是否具有这些值的子集。
例如,如果我们有一个 TypeA
,它描述了数字 1 到 3(联合 字面量类型),以及一个 TypeB
,它描述了数字 1 到 5:TypeA
将被视为 TypeB
的子类型,因为 TypeA
是 TypeB
的子集。
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
,它包含键 foo
和 bar
。那么 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
的子集。子类型必须接受至少与父类型相同的输入,并且必须返回最多与父类型相同的输出。
在输入和输出上应用子类型规则的方向的决定由变异控制,这是下一节的主题。