|
| 1 | +--- |
| 2 | +title: 条件类型 |
| 3 | +layout: docs |
| 4 | +permalink: /zh/docs/handbook/2/conditional-types.html |
| 5 | +oneline: "Create types which act like if statements in the type system." |
| 6 | +--- |
| 7 | + |
| 8 | +在大多数有用的程序的核心,我们必须根据输入做出决定。 |
| 9 | +JavaScript 程序也没有什么不同,但是考虑到值可以很容易地反思的事实,这些决定也是基于输入的类型。 |
| 10 | +_条件类型_ 有助于描述输入和输出类型之间的关系。 |
| 11 | + |
| 12 | +```ts twoslash |
| 13 | +interface Animal { |
| 14 | + live(): void; |
| 15 | +} |
| 16 | +interface Dog extends Animal { |
| 17 | + woof(): void; |
| 18 | +} |
| 19 | + |
| 20 | +type Example1 = Dog extends Animal ? number : string; |
| 21 | +// ^? |
| 22 | + |
| 23 | +type Example2 = RegExp extends Animal ? number : string; |
| 24 | +// ^? |
| 25 | +``` |
| 26 | + |
| 27 | +条件类型的形式看起来有点像 JavaScript 中的条件表达式(`条件 ? true 表达式 : false 表达式`): |
| 28 | + |
| 29 | +```ts twoslash |
| 30 | +type SomeType = any; |
| 31 | +type OtherType = any; |
| 32 | +type TrueType = any; |
| 33 | +type FalseType = any; |
| 34 | +type Stuff = |
| 35 | + // ---cut--- |
| 36 | + SomeType extends OtherType ? TrueType : FalseType; |
| 37 | +``` |
| 38 | + |
| 39 | +当 `extends` 左边的类型可以赋值给右边的类型时,你将获得第一个分支("true" 分支)中的类型;否则你将获得后一个分支("false" 分支)中的类型。 |
| 40 | + |
| 41 | +从上面的例子中,条件类型可能看起来不会立即有用 - 我们可以告诉自己是否 `Dog extends Animal` 并选择 `number` 或 `string`! |
| 42 | +但是条件类型的威力来自于将它们与泛型一起使用。 |
| 43 | + |
| 44 | +让我们以下面的 `createLabel` 函数为例: |
| 45 | + |
| 46 | +```ts twoslash |
| 47 | +interface IdLabel { |
| 48 | + id: number /* 一些字段 */; |
| 49 | +} |
| 50 | +interface NameLabel { |
| 51 | + name: string /* 其它字段 */; |
| 52 | +} |
| 53 | + |
| 54 | +function createLabel(id: number): IdLabel; |
| 55 | +function createLabel(name: string): NameLabel; |
| 56 | +function createLabel(nameOrId: string | number): IdLabel | NameLabel; |
| 57 | +function createLabel(nameOrId: string | number): IdLabel | NameLabel { |
| 58 | + throw "unimplemented"; |
| 59 | +} |
| 60 | +``` |
| 61 | + |
| 62 | +这些 createLabel 的重载描述了单个基于输入类型进行选择的 JavaScript 函数。注意以下几点: |
| 63 | + |
| 64 | +1. 如果一个库不得不在其 API 中一遍又一遍地做出相同的选择,这就变得很麻烦。 |
| 65 | +2. 我们必须创建三个重载:一种用于我们 _确定_ 类型时的每种情况(一个用于 `string`,一个用于 `number`),一个用于最一般的情况(接受一个 `string | number`)。对于 `createLabel` 可以处理的每个新类型,重载的数量都会呈指数增长。 |
| 66 | + |
| 67 | +相反,我们可以将该逻辑编码为条件类型: |
| 68 | + |
| 69 | +```ts twoslash |
| 70 | +interface IdLabel { |
| 71 | + id: number /* 一些字段 */; |
| 72 | +} |
| 73 | +interface NameLabel { |
| 74 | + name: string /* 其它字段 */; |
| 75 | +} |
| 76 | +// ---cut--- |
| 77 | +type NameOrId<T extends number | string> = T extends number |
| 78 | + ? IdLabel |
| 79 | + : NameLabel; |
| 80 | +``` |
| 81 | + |
| 82 | +然后,我们可以使用该条件类型将重载简化为没有重载的单个函数。 |
| 83 | + |
| 84 | +```ts twoslash |
| 85 | +interface IdLabel { |
| 86 | + id: number /* 一些字段 */; |
| 87 | +} |
| 88 | +interface NameLabel { |
| 89 | + name: string /* 其它字段 */; |
| 90 | +} |
| 91 | +type NameOrId<T extends number | string> = T extends number |
| 92 | + ? IdLabel |
| 93 | + : NameLabel; |
| 94 | +// ---cut--- |
| 95 | +function createLabel<T extends number | string>(idOrName: T): NameOrId<T> { |
| 96 | + throw "unimplemented"; |
| 97 | +} |
| 98 | + |
| 99 | +let a = createLabel("typescript"); |
| 100 | +// ^? |
| 101 | + |
| 102 | +let b = createLabel(2.8); |
| 103 | +// ^? |
| 104 | + |
| 105 | +let c = createLabel(Math.random() ? "hello" : 42); |
| 106 | +// ^? |
| 107 | +``` |
| 108 | + |
| 109 | +### 条件类型约束 |
| 110 | + |
| 111 | +通常,条件类型的检查将为我们提供一些新信息。 |
| 112 | +就像使用类型守卫缩小范围可以给我们提供更具体的类型一样,条件类型的 true 分支将根据我们检查的类型进一步约束泛型。 |
| 113 | + |
| 114 | +让我们来看看下面的例子: |
| 115 | + |
| 116 | +```ts twoslash |
| 117 | +// @errors: 2536 |
| 118 | +type MessageOf<T> = T["message"]; |
| 119 | +``` |
| 120 | + |
| 121 | +在本例中,TypeScript 错误是因为 `T` 不知道有一个名为 `message` 的属性。 |
| 122 | +我们可以限制 `T`,TypeScript 也不会再抱怨了: |
| 123 | + |
| 124 | +```ts twoslash |
| 125 | +type MessageOf<T extends { message: unknown }> = T["message"]; |
| 126 | + |
| 127 | +interface Email { |
| 128 | + message: string; |
| 129 | +} |
| 130 | + |
| 131 | +interface Dog { |
| 132 | + bark(): void; |
| 133 | +} |
| 134 | + |
| 135 | +type EmailMessageContents = MessageOf<Email>; |
| 136 | +// ^? |
| 137 | +``` |
| 138 | + |
| 139 | +然而,如果我们希望 `MessageOf` 采用任何类型,并且在 `message` 属性不可用的情况下缺省为 `never` 之类的类型,该怎么办呢? |
| 140 | +我们可以通过移出约束并引入条件类型来实现这一点: |
| 141 | + |
| 142 | +```ts twoslash |
| 143 | +type MessageOf<T> = T extends { message: unknown } ? T["message"] : never; |
| 144 | + |
| 145 | +interface Email { |
| 146 | + message: string; |
| 147 | +} |
| 148 | + |
| 149 | +interface Dog { |
| 150 | + bark(): void; |
| 151 | +} |
| 152 | + |
| 153 | +type EmailMessageContents = MessageOf<Email>; |
| 154 | +// ^? |
| 155 | + |
| 156 | +type DogMessageContents = MessageOf<Dog>; |
| 157 | +// ^? |
| 158 | +``` |
| 159 | + |
| 160 | +在 true 分支中,TypeScript 知道 `T` _将_ 有一个 `message` 属性。 |
| 161 | + |
| 162 | +作为另一个示例,我们还可以编写一个名为 `Flatten` 的类型,它将数组类型扁平为它们的元素类型,但在其他情况下不会处理它们: |
| 163 | + |
| 164 | +```ts twoslash |
| 165 | +type Flatten<T> = T extends any[] ? T[number] : T; |
| 166 | + |
| 167 | +// Extracts out the element type. |
| 168 | +type Str = Flatten<string[]>; |
| 169 | +// ^? |
| 170 | + |
| 171 | +// Leaves the type alone. |
| 172 | +type Num = Flatten<number>; |
| 173 | +// ^? |
| 174 | +``` |
| 175 | + |
| 176 | +当 `Flatten` 被赋予数组类型时,它使用带 `number` 的索引访问来提取 `string[]` 的元素类型。 |
| 177 | +否则,它只返回给定的类型。 |
| 178 | + |
| 179 | +### 在条件类型中推断 |
| 180 | + |
| 181 | +我们只是发现自己使用条件类型来应用约束,然后提取出类型。 |
| 182 | +这最终成为一种非常常见的操作,条件类型使其变得更容易。 |
| 183 | + |
| 184 | +条件类型为我们提供了一种使用 `infer` 关键字从 true 分支中与之进行比较的类型中进行推断的方法。 |
| 185 | +例如,我们可以在 `Flatten` 中推断元素类型,而不是使用索引访问类型“手动”提取它: |
| 186 | + |
| 187 | +```ts twoslash |
| 188 | +type Flatten<Type> = Type extends Array<infer Item> ? Item : Type; |
| 189 | +``` |
| 190 | + |
| 191 | +在这里,我们使用 `infer` 关键字以声明方式引入一个名为 `Item` 的新泛型类型变量,而不是指定如何在 true 分支中检索元素类型 `T`。 |
| 192 | +这使我们不必考虑如何挖掘和探索我们感兴趣的类型的结构。 |
| 193 | + |
| 194 | +我们可以使用 `infer` 关键字编写一些有用的助手类型别名。 |
| 195 | +例如,对于简单的情况,我们可以从函数类型中提取返回类型: |
| 196 | + |
| 197 | +```ts twoslash |
| 198 | +type GetReturnType<Type> = Type extends (...args: never[]) => infer Return |
| 199 | + ? Return |
| 200 | + : never; |
| 201 | + |
| 202 | +type Num = GetReturnType<() => number>; |
| 203 | +// ^? |
| 204 | + |
| 205 | +type Str = GetReturnType<(x: string) => string>; |
| 206 | +// ^? |
| 207 | + |
| 208 | +type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>; |
| 209 | +// ^? |
| 210 | +``` |
| 211 | + |
| 212 | +当从具有多个调用签名的类型(如重载函数的类型)进行推断时,将从 _最后一个_ 签名进行推断(这大概是最允许的捕获所有的情况)。无法基于参数类型列表执行重载解析。 |
| 213 | + |
| 214 | +```ts twoslash |
| 215 | +declare function stringOrNum(x: string): number; |
| 216 | +declare function stringOrNum(x: number): string; |
| 217 | +declare function stringOrNum(x: string | number): string | number; |
| 218 | + |
| 219 | +type T1 = ReturnType<typeof stringOrNum>; |
| 220 | +// ^? |
| 221 | +``` |
| 222 | + |
| 223 | +## 分配条件类型 |
| 224 | + |
| 225 | +当条件类型作用于泛型类型时,它们在给定联合类型时成为 _分配类型_ 。 |
| 226 | +以下面的例子为例: |
| 227 | + |
| 228 | +```ts twoslash |
| 229 | +type ToArray<Type> = Type extends any ? Type[] : never; |
| 230 | +``` |
| 231 | + |
| 232 | +如果我们将联合类型插入 `ToArray`,则条件类型将应用于该联合类型的每个成员。 |
| 233 | + |
| 234 | +```ts twoslash |
| 235 | +type ToArray<Type> = Type extends any ? Type[] : never; |
| 236 | + |
| 237 | +type StrArrOrNumArr = ToArray<string | number>; |
| 238 | +// ^? |
| 239 | +``` |
| 240 | + |
| 241 | +这里发生的情况是 `StrOrNumArray` 分布在以下位置: |
| 242 | + |
| 243 | +```ts twoslash |
| 244 | +type StrArrOrNumArr = |
| 245 | + // ---cut--- |
| 246 | + string | number; |
| 247 | +``` |
| 248 | + |
| 249 | +并在联合类型的每个成员类型上映射到有效的内容: |
| 250 | + |
| 251 | +```ts twoslash |
| 252 | +type ToArray<Type> = Type extends any ? Type[] : never; |
| 253 | +type StrArrOrNumArr = |
| 254 | + // ---cut--- |
| 255 | + ToArray<string> | ToArray<number>; |
| 256 | +``` |
| 257 | + |
| 258 | +所以我们只剩下: |
| 259 | + |
| 260 | +```ts twoslash |
| 261 | +type StrArrOrNumArr = |
| 262 | + // ---cut--- |
| 263 | + string[] | number[]; |
| 264 | +``` |
| 265 | + |
| 266 | +通常,分布性是所需的行为。 |
| 267 | +要避免这种行为,可以用方括号括起 `extends` 关键字的两边。 |
| 268 | + |
| 269 | +```ts twoslash |
| 270 | +type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never; |
| 271 | + |
| 272 | +// 'StrOrNumArr' 不再是一个联合类型 |
| 273 | +type StrOrNumArr = ToArrayNonDist<string | number>; |
| 274 | +// ^? |
| 275 | +``` |
0 commit comments