|
| 1 | +--- |
| 2 | +title: Conditional Types |
| 3 | +layout: docs |
| 4 | +permalink: /docs/handbook/2/conditional-types.html |
| 5 | +oneline: "Create types which act like if statements in the type system." |
| 6 | +--- |
| 7 | + |
| 8 | +At the heart of most useful programs, we have to make decisions based on input. |
| 9 | +JavaScript programs are no different, but given the fact that values can be easily introspected, those decisions are also based on the types of the inputs. |
| 10 | +_Conditional types_ help describe the relation between the types of inputs and outputs. |
| 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 | +Conditional types take a form that looks a little like conditional expressions (`condition ? trueExpression : falseExpression`) in JavaScript: |
| 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 | +When the type on the left of the `extends` is assignable to the one on the right, then you'll get the type in the first branch (the "true" branch); otherwise you'll get the type in the latter branch (the "false" branch). |
| 40 | + |
| 41 | +From the examples above, conditional types might not immediately seem useful - we can tell ourselves whether or not `Dog extends Animal` and pick `number` or `string`! |
| 42 | +But the power of conditional types comes from using them with generics. |
| 43 | + |
| 44 | +For example, let's take the following `createLabel` function: |
| 45 | + |
| 46 | +```ts twoslash |
| 47 | +interface IdLabel { |
| 48 | + id: number /* some fields */; |
| 49 | +} |
| 50 | +interface NameLabel { |
| 51 | + name: string /* other fields */; |
| 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 | +These overloads for createLabel describe a single JavaScript function that makes a choice based on the types of its inputs. Note a few things: |
| 63 | + |
| 64 | +1. If a library has to make the same sort of choice over and over throughout its API, this becomes cumbersome. |
| 65 | +2. We have to create three overloads: one for each case when we're _sure_ of the type (one for `string` and one for `number`), and one for the most general case (taking a `string | number`). For every new type `createLabel` can handle, the number of overloads grows exponentially. |
| 66 | + |
| 67 | +Instead, we can encode that logic in a conditional type: |
| 68 | + |
| 69 | +```ts twoslash |
| 70 | +interface IdLabel { |
| 71 | + id: number /* some fields */; |
| 72 | +} |
| 73 | +interface NameLabel { |
| 74 | + name: string /* other fields */; |
| 75 | +} |
| 76 | +// ---cut--- |
| 77 | +type NameOrId<T extends number | string> = T extends number |
| 78 | + ? IdLabel |
| 79 | + : NameLabel; |
| 80 | +``` |
| 81 | + |
| 82 | +We can then use that conditional type to simplify our overloads down to a single function with no overloads. |
| 83 | + |
| 84 | +```ts twoslash |
| 85 | +interface IdLabel { |
| 86 | + id: number /* some fields */; |
| 87 | +} |
| 88 | +interface NameLabel { |
| 89 | + name: string /* other fields */; |
| 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 | +### Conditional Type Constraints |
| 110 | + |
| 111 | +Often, the checks in a conditional type will provide us with some new information. |
| 112 | +Just like with narrowing with type guards can give us a more specific type, the true branch of a conditional type will further constrain generics by the type we check against. |
| 113 | + |
| 114 | +For example, let's take the following: |
| 115 | + |
| 116 | +```ts twoslash |
| 117 | +// @errors: 2536 |
| 118 | +type MessageOf<T> = T["message"]; |
| 119 | +``` |
| 120 | + |
| 121 | +In this example, TypeScript errors because `T` isn't known to have a property called `message`. |
| 122 | +We could constrain `T`, and TypeScript would no longer complain: |
| 123 | + |
| 124 | +```ts twoslash |
| 125 | +type MessageOf<T extends { message: unknown }> = T["message"]; |
| 126 | + |
| 127 | +interface Email { |
| 128 | + message: string; |
| 129 | +} |
| 130 | + |
| 131 | +type EmailMessageContents = MessageOf<Email>; |
| 132 | +// ^? |
| 133 | +``` |
| 134 | + |
| 135 | +However, what if we wanted `MessageOf` to take any type, and default to something like `never` if a `message` property isn't available? |
| 136 | +We can do this by moving the constraint out and introducing a conditional type: |
| 137 | + |
| 138 | +```ts twoslash |
| 139 | +type MessageOf<T> = T extends { message: unknown } ? T["message"] : never; |
| 140 | + |
| 141 | +interface Email { |
| 142 | + message: string; |
| 143 | +} |
| 144 | + |
| 145 | +interface Dog { |
| 146 | + bark(): void; |
| 147 | +} |
| 148 | + |
| 149 | +type EmailMessageContents = MessageOf<Email>; |
| 150 | +// ^? |
| 151 | + |
| 152 | +type DogMessageContents = MessageOf<Dog>; |
| 153 | +// ^? |
| 154 | +``` |
| 155 | + |
| 156 | +Within the true branch, TypeScript knows that `T` _will_ have a `message` property. |
| 157 | + |
| 158 | +As another example, we could also write a type called `Flatten` that flattens array types to their element types, but leaves them alone otherwise: |
| 159 | + |
| 160 | +```ts twoslash |
| 161 | +type Flatten<T> = T extends any[] ? T[number] : T; |
| 162 | + |
| 163 | +// Extracts out the element type. |
| 164 | +type Str = Flatten<string[]>; |
| 165 | +// ^? |
| 166 | + |
| 167 | +// Leaves the type alone. |
| 168 | +type Num = Flatten<number>; |
| 169 | +// ^? |
| 170 | +``` |
| 171 | + |
| 172 | +When `Flatten` is given an array type, it uses an indexed access with `number` to fetch out `string[]`'s element type. |
| 173 | +Otherwise, it just returns the type it was given. |
| 174 | + |
| 175 | +### Inferring Within Conditional Types |
| 176 | + |
| 177 | +We just found ourselves using conditional types to apply constraints and then extract out types. |
| 178 | +This ends up being such a common operation that conditional types make it easier. |
| 179 | + |
| 180 | +Conditional types provide us with a way to infer from types we compare against in the true branch using the `infer` keyword. |
| 181 | +For example, we could have inferred the element type in `Flatten` instead of fetching it out "manually" with an indexed access type: |
| 182 | + |
| 183 | +```ts twoslash |
| 184 | +type Flatten<Type> = Type extends Array<infer Item> ? Item : Type; |
| 185 | +``` |
| 186 | + |
| 187 | +Here, we used the `infer` keyword to declaratively introduce a new generic type variable named `Item` instead of specifying how to retrieve the element type of `T` within the true branch. |
| 188 | +This frees us from having to think about how to dig through and probing apart the structure of the types we're interested in. |
| 189 | + |
| 190 | +We can write some useful helper type aliases using the `infer` keyword. |
| 191 | +For example, for simple cases, we can extract the return type out from function types: |
| 192 | + |
| 193 | +```ts twoslash |
| 194 | +type GetReturnType<Type> = Type extends (...args: never[]) => infer Return |
| 195 | + ? Return |
| 196 | + : never; |
| 197 | + |
| 198 | +type Num = GetReturnType<() => number>; |
| 199 | +// ^? |
| 200 | + |
| 201 | +type Str = GetReturnType<(x: string) => string>; |
| 202 | +// ^? |
| 203 | + |
| 204 | +type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>; |
| 205 | +// ^? |
| 206 | +``` |
| 207 | + |
| 208 | +When inferring from a type with multiple call signatures (such as the type of an overloaded function), inferences are made from the _last_ signature (which, presumably, is the most permissive catch-all case). It is not possible to perform overload resolution based on a list of argument types. |
| 209 | + |
| 210 | +```ts twoslash |
| 211 | +declare function stringOrNum(x: string): number; |
| 212 | +declare function stringOrNum(x: number): string; |
| 213 | +declare function stringOrNum(x: string | number): string | number; |
| 214 | + |
| 215 | +type T1 = ReturnType<typeof stringOrNum>; |
| 216 | +// ^? |
| 217 | +``` |
| 218 | + |
| 219 | +## Distributive Conditional Types |
| 220 | + |
| 221 | +When conditional types act on a generic type, they become _distributive_ when given a union type. |
| 222 | +For example, take the following: |
| 223 | + |
| 224 | +```ts twoslash |
| 225 | +type ToArray<Type> = Type extends any ? Type[] : never; |
| 226 | +``` |
| 227 | + |
| 228 | +If we plug a union type into `ToArray`, then the conditional type will be applied to each member of that union. |
| 229 | + |
| 230 | +```ts twoslash |
| 231 | +type ToArray<Type> = Type extends any ? Type[] : never; |
| 232 | + |
| 233 | +type StrArrOrNumArr = ToArray<string | number>; |
| 234 | +// ^? |
| 235 | +``` |
| 236 | + |
| 237 | +What happens here is that `StrArrOrNumArr ` distributes on: |
| 238 | + |
| 239 | +```ts twoslash |
| 240 | +type StrArrOrNumArr = |
| 241 | + // ---cut--- |
| 242 | + string | number; |
| 243 | +``` |
| 244 | + |
| 245 | +and maps over each member type of the union, to what is effectively: |
| 246 | + |
| 247 | +```ts twoslash |
| 248 | +type ToArray<Type> = Type extends any ? Type[] : never; |
| 249 | +type StrArrOrNumArr = |
| 250 | + // ---cut--- |
| 251 | + ToArray<string> | ToArray<number>; |
| 252 | +``` |
| 253 | + |
| 254 | +which leaves us with: |
| 255 | + |
| 256 | +```ts twoslash |
| 257 | +type StrArrOrNumArr = |
| 258 | + // ---cut--- |
| 259 | + string[] | number[]; |
| 260 | +``` |
| 261 | + |
| 262 | +Typically, distributivity is the desired behavior. |
| 263 | +To avoid that behavior, you can surround each side of the `extends` keyword with square brackets. |
| 264 | + |
| 265 | +```ts twoslash |
| 266 | +type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never; |
| 267 | + |
| 268 | +// 'StrArrOrNumArr' is no longer a union. |
| 269 | +type StrArrOrNumArr = ToArrayNonDist<string | number>; |
| 270 | +// ^? |
| 271 | +``` |
0 commit comments