You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
// In here, the compiler knows that `shape` is a `Triangle`,
81
-
// so we can access triangle-specific properties
68
+
// so we can access triangle-specific properties.
69
+
// See for yourself: hover each occurance of “shape” and
70
+
// compare the typing info.
82
71
console.log(shape.triangleKind);
83
72
} else {
84
73
// In here, the compiler knows that `shape` is a `Quadrilateral`.
85
-
console.log(shape.quadrilateralFlags);
74
+
console.log(shape.isRectangle);
86
75
}
87
76
}
88
77
```
89
78
90
-
When we have a union (like `Triangle | Quadrilateral`) that can be narrowed by a specific property (like `numberOfSides`), that union is called a _discriminated union_ and that property is called the _discriminant property_.
79
+
When we have a union (like `Triangle | Quadrilateral`) that can be narrowed by a literal member (like `numberOfSides`), that union is called a _discriminated union_ and that property is called the _discriminant property_.
91
80
92
-
## Do these props look too loose on me?
93
-
You’re writing a Select component (i.e., a fancy replacement for an HTMLSelectElement) with React and TypeScript. Perhaps you look at the [`SelectHTMLAttributes` interface](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/eda212cfd64119cf2edc2f4ab12e53c4654a9b87/types/react/index.d.ts#L1979-L1990) from [`@types/react`](https://www.npmjs.com/package/@types/react) for inspiration, and notice that a native select element, in React, can have a `value` of type `string | string[] | number`. From TypeScript’s perspective, you can pass a single value or an array of values indiscriminately, but you know that an array of values is really only meaningful if the `multiple` prop is set. Nonetheless, you try this approach for your component:
81
+
## The problem: Overly permissive props
82
+
You’re writing a Select component (i.e., a fancy replacement for an HTMLSelectElement) with React and TypeScript. You want it to support both single-selection and multiple-selection, just like a native select element. Perhaps you look at the [`SelectHTMLAttributes` interface](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/eda212cfd64119cf2edc2f4ab12e53c4654a9b87/types/react/index.d.ts#L1979-L1990) from [`@types/react`](https://www.npmjs.com/package/@types/react) for inspiration, and notice that a native select element, in React, can have a `value` of type `string | string[] | number`. From TypeScript’s perspective, you can pass a single value or an array of values indiscriminately, but you know that an array of values is really only meaningful if the `multiple` prop is set. Nonetheless, you try this approach for your component:
94
83
95
84
<!--@
96
85
name: select-1.tsx
@@ -115,7 +104,8 @@ The idea is that when `multiple` is `true`, the consumer should set `value` to a
115
104
name: select-1.tsx
116
105
-->
117
106
```tsx
118
-
// Missing `multiple` prop, but no compiler error
107
+
// Value is an array, but it’s missing the `multiple`
108
+
// prop, but no compiler error
119
109
<Select
120
110
options={['Red', 'Green', 'Blue']}
121
111
value={['Red', 'Blue']}
@@ -174,7 +164,7 @@ class Select extends React.Component<SelectProps> {
174
164
}
175
165
```
176
166
177
-
As triangles and quadrilaterals can be distinguished by their number of sides, the union type `SelectProps` can be discriminated by its `multiple` property. And as luck would have it, TypeScript will do exactly that when you pass (or don’t pass) the `multiple` prop to your new and improved component:
167
+
As triangles and quadrilaterals can be distinguished by their number of sides, the union type `SelectProps` can be discriminated by its `multiple` property. And as luck would have it, TypeScript will do exactly that when you pass (or don’t pass) the `multiple` prop to your new and improved component:[^1]
178
168
179
169
<!--@
180
170
name: select-2.tsx
@@ -209,7 +199,7 @@ name: select-2.tsx
209
199
Whoa, this is a bazillion times better! Nice work; consumers of your component will thank you for coaching them down the right path _before_ they run their code in a browser. 🎉
210
200
211
201
## Going deeper with the distributive law of sets
212
-
Time goes by. Your Select component was a big hit with the other developers who were using it. Maybe you got a promotion. But then, the design team shows you specs for a Select component with _groups_ of options, with customizable titles for each group. You start prototyping the props you’ll have to add in your head:
202
+
Time goes by. Your Select component was a big hit with the other developers who were using it. But then, the design team shows you specs for a Select component with _groups_ of options, with customizable titles for each group. You start prototyping the props you’ll have to add in your head:
@@ -233,7 +223,7 @@ With two different choices to make (multiple and grouped), each with two options
233
223
3. single selection, grouped
234
224
4. multiple selection, grouped.
235
225
236
-
Writing each of those options out as a complete interface of possible Select props and creating a union of all four isn’t unthinkably tedious, but the relationship is exponential: three boolean choices makes a union of 2^3 = 8, four choices is 16, and so on. Rather sooner than later, it becomes unwieldy to express every combination of essentially unrelated choices explicitly.
226
+
Writing each of those options out as a complete interface of possible Select props and creating a union of all four isn’t unthinkably tedious, but the relationship is exponential: three boolean choices makes a union of $2^3 = 8$, four choices is 16, and so on. Rather sooner than later, it becomes unwieldy to express every combination of essentially unrelated choices explicitly.
237
227
238
228
You can avoid repeating yourself and writing out every combination by taking advantage of some set theory. Instead of writing four complete interfaces that repeat props from each other, you can write interfaces for each discrete piece of functionality and combine them via intersection:
239
229
@@ -285,7 +275,7 @@ class Select extends React.Component<SelectProps> {
285
275
286
276
Let’s break down what happened here:
287
277
288
-
1. For each constituent in the union, we removed its `extends` clause so the interface reflects only a discrete subset of functionality that can be intersected cleanly with anything else. (In this example, that’s not strictly necessary, but I think it’s cleaner, and I have an unverified theory that it’s less work for the compiler.[^1]) To reflect this change in our naming, we also suffixed each interface with `Fragment` to be clear that it’s not a complete working set of Select props.
278
+
1. For each constituent in the union, we removed its `extends` clause so the interface reflects only a discrete subset of functionality that can be intersected cleanly with anything else. (In this example, that’s not strictly necessary, but I think it’s cleaner, and I have an unverified theory that it’s less work for the compiler.[^2]) To reflect this change in our naming, we also suffixed each interface with `Fragment` to be clear that it’s not a complete working set of Select props.
289
279
2. We broke down grouped and non-grouped selects into two interfaces discriminated on `grouped`, just like we did before with `multiple`.
290
280
3. We combined everything together with an intersection of unions. In plain English, SelectProps is made up of:
291
281
- `CommonSelectProps`, along with
@@ -307,7 +297,7 @@ $$
307
297
If, like me, you haven’t studied computer science in an academic setting, this may look intimidatingly theoretical, but quickly make the following mental substitutions:
308
298
309
299
- Set theory’s union operator, $\cup$, is written as `|` in TypeScript
310
-
- Set theory’s intersection operator, $\cap$, is written as `&` in TypeScript[^2]
300
+
- Set theory’s intersection operator, $\cap$, is written as `&` in TypeScript[^3]
311
301
- Let $Z =$ `CommonSelectProps`
312
302
- Let $A =$ `SingleSelectPropsFragment`
313
303
- Let $B =$ `MultipleSelectPropsFragment`
@@ -356,8 +346,10 @@ Discriminated unions can be a powerful tool for writing better React component t
356
346
-[Tagged union - Wikipedia](https://en.wikipedia.org/wiki/Tagged_union)
357
347
358
348
[^1]:
359
-
My hypothesis is that in calculating the intersection of _N_ types that all include common properties, the compiler must calculate for each of _n_ common properties of type _T_ that _T_ intersected with itself _N_ times is still _T_. This is surely not a computationally expensive code path, but unless there’s a clever short circuit early in the calculation, it still has to happen _N ⨉ n_ times, all of which are unnecessary. This is purely unscientific speculation, and I would be happy for someone to correct or corroborate this theory.
349
+
Interestingly, in the final case here, the explicit value `multiple={false}`is required not to pass type checking, but to get accurate inference on the argument to `onChange`. This seems like a limitation/bug to me.
360
350
[^2]:
361
-
This statement applies only in the type declaration space. `|` and `&` are bitwise operators in the variable declaration space. E.g., `|` is the union operator in `var x: string | number` but the bitwise _or_ operator in `var x = 0xF0 | 0x0F`.
351
+
My hypothesis is that in calculating the intersection of _N_ types that all include common properties, the compiler must calculate for each of _n_ common properties of type _T_ that _T_ intersected with itself _N_ times is still _T_. This is surely not a computationally expensive code path, but unless there’s a clever short circuit early in the calculation, it still has to happen _N ⨉ n_ times, all of which are unnecessary. This is purely unscientific speculation, and I would be happy for someone to correct or corroborate this theory.
362
352
[^3]:
353
+
This statement applies only in the type declaration space. `|` and `&` are bitwise operators in the variable declaration space. E.g., `|` is the union operator in `var x: string | number` but the bitwise _or_ operator in `var x = 0xF0 | 0x0F`.
354
+
[^4]:
363
355
TypeScript does successfully discriminate between these constituents, but type inference [is currently broken](https://github.com/Microsoft/TypeScript/issues/29340) for properties that have different function signatures in different constituents when any of those constituents are an intersection type.
0 commit comments