Skip to content
This repository was archived by the owner on Nov 16, 2023. It is now read-only.

Commit 1a21a7f

Browse files
Added docs for conditional types.
1 parent 57732ae commit 1a21a7f

File tree

2 files changed

+250
-1
lines changed

2 files changed

+250
-1
lines changed
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
# Types from Transformation
2+
3+
There are certain patterns that are very commonplace in JavaScript, like iterating over the keys of objects to create new ones, and returning different values based on the inputs given to us.
4+
This idea of creating new values and types on the fly is somewhat untraditional in typed languages, but TypeScript provides some useful base constructs in the type system to accurately model that behavior, much in the same way that `keyof` can be used to discuss the property names of objects, and indexed access types can be used to fetch values of a certain property name.
5+
6+
We'll quickly see that combined, these smaller constructs can be surprisingly powerful and can express many patterns in the JavaScript ecosystem.
7+
8+
## Conditional Types
9+
10+
At the heart of most useful programs, we have to make decisions based on input.
11+
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.
12+
*Conditional types* help describe the relation between the types of inputs and outputs.
13+
14+
```ts
15+
interface Animal {
16+
live(): void;
17+
}
18+
interface Dog extends Animal {
19+
woof(): void;
20+
}
21+
22+
type Foo = Dog extends Animal ? number : string;
23+
^?
24+
25+
type Bar = RegExp extends Animal ? number : string;
26+
^?
27+
```
28+
29+
Conditional types take a form that looks a little like conditional expresions (`cond ? trueExpression : falseExpression`) in JavaScript:
30+
31+
```ts
32+
type SomeType = any;
33+
type OtherType = any;
34+
type TrueType = any;
35+
type FalseType = any;
36+
type Stuff =
37+
//cut
38+
SomeType extends OtherType ? TrueType : FalseType
39+
```
40+
41+
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).
42+
43+
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`!
44+
But the power of conditional types comes from using them with generics.
45+
46+
For example, let's take the following `createLabel` function:
47+
48+
```ts
49+
interface IdLabel { id: number, /* some fields */ }
50+
interface NameLabel { name: string, /* other fields */ }
51+
52+
function createLabel(id: number): IdLabel;
53+
function createLabel(name: string): NameLabel;
54+
function createLabel(nameOrId: string | number): IdLabel | NameLabel;
55+
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
56+
throw "unimplemented";
57+
}
58+
```
59+
60+
These overloads for createLabel describe a single JavaScript function that makes a choice based on the types of its inputs. Note a few things:
61+
62+
1. If a library has to make the same sort of choice over and over throughout its API, this becomes cumbersome.
63+
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.
64+
65+
Instead, we can encode that logic in a conditional type:
66+
67+
```ts
68+
interface IdLabel { id: number, /* some fields */ }
69+
interface NameLabel { name: string, /* other fields */ }
70+
//cut
71+
type NameOrId<T extends number | string> =
72+
T extends number ? IdLabel : NameLabel;
73+
```
74+
75+
We can then use that conditional type to simplify out overloads down to a single function with no overloads.
76+
77+
```ts
78+
interface IdLabel { id: number, /* some fields */ }
79+
interface NameLabel { name: string, /* other fields */ }
80+
type NameOrId<T extends number | string> =
81+
T extends number ? IdLabel : NameLabel;
82+
//cut
83+
function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
84+
throw "unimplemented"
85+
}
86+
87+
let a = createLabel("typescript");
88+
^?
89+
90+
let b = createLabel(2.8);
91+
^?
92+
93+
let c = createLabel(Math.random() ? "hello" : 42);
94+
^?
95+
```
96+
97+
### Conditional Type Constraints
98+
99+
Often, the checks in a conditional type will provide us with some new information.
100+
Just like with narrowing with type guards can give us a more specific type, the true branch of a conditional type will further constraint generics by the type we check against.
101+
102+
For example, let's take the following:
103+
104+
```ts
105+
type MessageOf<T> = T["message"];
106+
```
107+
108+
In this example, TypeScript errors because `T` isn't known to have a property called `message`.
109+
We could constrain `T`, and TypeScript would no longer complain:
110+
111+
```ts
112+
type MessageOf<T extends { message: unknown }> = T["message"];
113+
114+
interface Email {
115+
message: string;
116+
}
117+
118+
interface Dog {
119+
bark(): void;
120+
}
121+
122+
type EmailMessageContents = MessageOf<Email>;
123+
^?
124+
```
125+
126+
However, what if we wanted `MessageOf` to take any type, and default to something like `never` if a `message` property isn't available?
127+
We can do this by moving the constraint out and introducing a conditional type:
128+
129+
```ts
130+
type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
131+
132+
interface Email { message: string }
133+
134+
interface Dog { bark(): void }
135+
136+
type EmailMessageContents = MessageOf<Email>;
137+
^?
138+
139+
type DogMessageContents = MessageOf<Dog>;
140+
^?
141+
```
142+
143+
Within the true branch, TypeScript knows that `T` *will* have a `message` property.
144+
145+
As another example, we could also write a type called `Flatten` that flattens array types to their element types, but leaves them alone otherwise:
146+
147+
```ts
148+
type Flatten<T> = T extends any[] ? T[number] : T
149+
150+
// Extracts out the element type.
151+
type Str = Flatten<string[]>;
152+
^?
153+
154+
// Leaves the type alone.
155+
type Num = Flatten<number>;
156+
^?
157+
```
158+
159+
When `Flatten` is given an array type, it uses an indexed access with `number` to fetch out `string[]`'s element type.
160+
Otherwise, it just returns the type it was given.
161+
162+
### Inferring Within Conditional Types
163+
164+
We just found ourselves using conditional types to apply constraints and then extract out types.
165+
This ends up being such a common operation that conditional types make it easier.
166+
167+
Conditional types provide us with a way to infer from types we compare against in the true branch using the `infer` keyword.
168+
For example, we could have inferred the element type in `Flatten` instead of fetching it out "manually" with an indexed access type:
169+
170+
```ts
171+
type Flatten<T> = T extends Array<infer U> ? U : T;
172+
```
173+
174+
Here, we used the `infer` keyword declaratively introduced a new generic type variable named `U` instead of specifying how to retrieve the element type of `T`.
175+
Within the true branch
176+
This frees us from having to think about how to dig through and probing apart the structure of the types we're interested.
177+
178+
We can write some useful helper type aliases using the `infer` keyword.
179+
For example, for simple cases, we can extract the return type out from function types:
180+
181+
```ts
182+
type GetReturnType<T> =
183+
T extends (...args: never[]) => infer U ? U : never;
184+
185+
type Foo = GetReturnType<() => number>;
186+
^?
187+
188+
type Bar = GetReturnType<(x: string) => string>;
189+
^?
190+
191+
type Baz = GetReturnType<(a: boolean, b: boolean) => boolean[]>;
192+
^?
193+
```
194+
195+
## Distributive Conditional Types
196+
197+
When conditional types act on a generic type, they become *distributive* when given a union type.
198+
For example, take the following:
199+
200+
```ts
201+
type Foo<T> = T extends any ? T[] : never;
202+
```
203+
204+
If we plug a union type into `Foo`, then the conditional type will be applied to each member of that union.
205+
206+
```ts
207+
type Foo<T> = T extends any ? T[] : never;
208+
209+
type Bar = Foo<string | number>;
210+
^?
211+
```
212+
213+
What happens here is that `Foo` distributes on
214+
215+
```ts
216+
type Blah =
217+
//cut
218+
string | number
219+
```
220+
221+
and maps over each member type of the union, to what is effectively
222+
223+
```ts
224+
type Foo<T> = T extends any ? T[] : never;
225+
type Blah =
226+
//cut
227+
Foo<string> | Foo<number>
228+
```
229+
230+
which leaves us with
231+
232+
```ts
233+
type Blah =
234+
//cut
235+
string[] | number[]
236+
```
237+
238+
Typically, distributivity is the desired behavior.
239+
To avoid that behavior, you can surround each side of the `extends` keyword with square brackets.
240+
241+
```ts
242+
type Foo<T> = [T] extends [any] ? T[] : never;
243+
244+
// 'Bar' is no longer a union.
245+
type Bar = Foo<string | number>;
246+
^?
247+
```

meta/TODO.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@
99
* Turn on a linter
1010
* Enable `[section links]`
1111
* Disable text selection from tooltips
12-
* Semantic HTML, and tag parsing
12+
* Semantic HTML, and tag parsing
13+
* Weird highlighting for `T extends (...args: never[]) => infer U ? U : never;`
14+
* Are the classifiers fighting with each other?

0 commit comments

Comments
 (0)