Skip to content
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待

条件类型

在大多数有用的程序核心中,我们必须根据输入做出决策。 JavaScript 程序也不例外,但鉴于值可以很容易地被自省,这些决策也基于输入的类型。 条件类型帮助描述输入和输出类型之间的关系。

ts
interface Animal {
  
live
(): void;
} interface Dog extends Animal {
woof
(): void;
} type
Example1
= Dog extends Animal ? number : string;
type
Example2
= RegExp extends Animal ? number : string;
Try

条件类型的形式看起来有点像 JavaScript 中的条件表达式(condition ? trueExpression : falseExpression):

ts
  
SomeType
extends
OtherType
?
TrueType
:
FalseType
;
Try

extends 左边的类型可以赋值给右边的类型时,你将获得第一个分支(“true”分支)中的类型;否则你将获得后一个分支(“false”分支)中的类型。

从上面的例子来看,条件类型可能看起来并不是立即有用的——我们可以自己判断 Dog extends Animal 是否成立,然后选择 numberstring! 但条件类型的强大之处在于将它们与泛型一起使用。

例如,让我们来看下面的 createLabel 函数:

ts
interface IdLabel {
  
id
: number /* some fields */;
} interface NameLabel {
name
: string /* other fields */;
} function
createLabel
(
id
: number): IdLabel;
function
createLabel
(
name
: string): NameLabel;
function
createLabel
(
nameOrId
: string | number): IdLabel | NameLabel;
function
createLabel
(
nameOrId
: string | number): IdLabel | NameLabel {
throw "unimplemented"; }
Try

这些 createLabel 的重载描述了一个单一的 JavaScript 函数,该函数根据其输入的类型做出选择。注意几点:

  1. 如果一个库必须在其 API 中反复做出相同类型的选择,这会变得很繁琐。
  2. 我们必须创建三个重载:一个用于当我们确定类型的情况(一个用于 string,一个用于 number),还有一个用于最一般的情况(接受 string | number)。对于 createLabel 可以处理的每一种新类型,重载的数量呈指数级增长。

相反,我们可以将该逻辑编码在一个条件类型中:

ts
type 
NameOrId
<
T
extends number | string> =
T
extends number
? IdLabel : NameLabel;
Try

然后我们可以使用该条件类型将我们的重载简化为一个没有重载的函数。

ts
function 
createLabel
<
T
extends number | string>(
idOrName
:
T
):
NameOrId
<
T
> {
throw "unimplemented"; } let
a
=
createLabel
("typescript");
let
b
=
createLabel
(2.8);
let
c
=
createLabel
(
Math
.
random
() ? "hello" : 42);
Try

条件类型约束

通常,条件类型中的检查会为我们提供一些新信息。 就像使用类型守卫进行 narrowing 可以给我们一个更具体的类型一样,条件类型的 true 分支将通过我们检查的类型进一步约束泛型。

例如,让我们看下面的代码:

ts
type 
MessageOf
<
T
> = T["message"];
Type '"message"' cannot be used to index type 'T'.
Try

在这个例子中,TypeScript 报错,因为不知道 T 有一个名为 message 的属性。 我们可以约束 T,这样 TypeScript 就不再报错:

ts
type 
MessageOf
<
T
extends {
message
: unknown }> =
T
["message"];
interface Email {
message
: string;
} type
EmailMessageContents
=
MessageOf
<Email>;
Try

但是,如果我们希望 MessageOf 接受任何类型,并且如果没有 message 属性则默认为类似 never 的类型,该怎么办? 我们可以通过将约束移出并引入条件类型来实现:

ts
type 
MessageOf
<
T
> =
T
extends {
message
: unknown } ?
T
["message"] : never;
interface Email {
message
: string;
} interface Dog {
bark
(): void;
} type
EmailMessageContents
=
MessageOf
<Email>;
type
DogMessageContents
=
MessageOf
<Dog>;
Try

在 true 分支中,TypeScript 知道 T 将会有一个 message 属性。

作为另一个例子,我们还可以编写一个名为 Flatten 的类型,它将数组类型展平为其元素类型,否则保持不变:

ts
type 
Flatten
<
T
> =
T
extends any[] ?
T
[number] :
T
;
// 提取出元素类型。 type
Str
=
Flatten
<string[]>;
// 保持原类型不变。 type
Num
=
Flatten
<number>;
Try

Flatten 被赋予一个数组类型时,它使用带有 number 的索引访问来获取 string[] 的元素类型。 否则,它只是返回给定的类型。

在条件类型中推断

我们刚刚发现自己使用条件类型来应用约束,然后提取类型。 这最终成为一种常见的操作,以至于条件类型使其变得更加容易。

条件类型为我们提供了一种使用 infer 关键字从我们在 true 分支中比较的类型中进行推断的方法。 例如,我们可以在 Flatten 中推断元素类型,而不是使用索引访问类型“手动”获取它:

ts
type 
Flatten
<
Type
> =
Type
extends
Array
<infer
Item
> ?
Item
:
Type
;
Try

在这里,我们使用 infer 关键字以声明方式引入了一个新的泛型类型变量 Item,而不是指定如何在 true 分支中检索 Type 的元素类型。 这使我们不必考虑如何深入挖掘和探测我们感兴趣类型的结构。

我们可以使用 infer 关键字编写一些有用的辅助类型别名。 例如,对于简单的情况,我们可以从函数类型中提取返回类型:

ts
type 
GetReturnType
<
Type
> =
Type
extends (...
args
: never[]) => infer
Return
?
Return
: never; type
Num
=
GetReturnType
<() => number>;
type
Str
=
GetReturnType
<(
x
: string) => string>;
type
Bools
=
GetReturnType
<(
a
: boolean,
b
: boolean) => boolean[]>;
Try

当从具有多个调用签名的类型(例如重载函数的类型)进行推断时,推断是从最后一个签名进行的(该签名大概是包罗万象的最宽松的情况)。不可能基于参数类型列表执行重载解析。

ts
declare function 
stringOrNum
(
x
: string): number;
declare function
stringOrNum
(
x
: number): string;
declare function
stringOrNum
(
x
: string | number): string | number;
type
T1
=
ReturnType
<typeof
stringOrNum
>;
Try

分布式条件类型

当条件类型作用于泛型类型时,当给定一个联合类型时,它们会变成分布式的。 例如,看下面的代码:

ts
type 
ToArray
<
Type
> =
Type
extends any ?
Type
[] : never;
Try

如果我们将一个联合类型插入 ToArray,那么条件类型将应用于该联合的每个成员。

ts
type 
ToArray
<
Type
> =
Type
extends any ?
Type
[] : never;
type
StrArrOrNumArr
=
ToArray
<string | number>;
Try

这里发生的事情是 ToArray 分布在:

ts
  string | number;
Try

并映射联合的每个成员类型,实际上等同于:

ts
  
ToArray
<string> |
ToArray
<number>;
Try

这给我们留下了:

ts
  string[] | number[];
Try

通常,分布性是期望的行为。 要避免这种行为,你可以用方括号括起 extends 关键字的每一侧。

ts
type 
ToArrayNonDist
<
Type
> = [
Type
] extends [any] ?
Type
[] : never;
// 'ArrOfStrOrNum' 不再是联合类型。 type
ArrOfStrOrNum
=
ToArrayNonDist
<string | number>;
Try