|
| 1 | +# Widening and Narrowing in Typescript |
| 2 | + |
| 3 | +Typescript has a number of related concepts in which a type gets |
| 4 | +treated temporarily as a similar type. Most of these concepts are |
| 5 | +internal-only. None of them are documented very well. For the internal |
| 6 | +concepts, we expect nobody needs to know about them to use the |
| 7 | +language. For the external concepts, we hope that they work well |
| 8 | +enough that most people *still* don't need to think about them. This |
| 9 | +document explains them all, aiming to help two audiences: (1) advanced |
| 10 | +users of Typescript who *do* need to understand the quirks of the |
| 11 | +language (2) contributors to the Typescript compiler. |
| 12 | + |
| 13 | +The concepts covered in this document are as follows: |
| 14 | + |
| 15 | +1. Widening: treat an internal type as a normal one. |
| 16 | +2. Literal widening: treat a literal type as a primitive one. |
| 17 | +3. Narrowing: remove constituents from a union type. |
| 18 | +4. Instanceof narrowing: treat a type as a subclass. |
| 19 | +5. Apparent type: treat a non-object type as an object type. |
| 20 | + |
| 21 | +## Widening |
| 22 | + |
| 23 | +Widening is the simplest operation of the bunch. The types `null` and |
| 24 | +`undefined` are converted to `any`. This happens |
| 25 | +recursively in object types, union types, and array types (including |
| 26 | +tuples). |
| 27 | + |
| 28 | +Why widening? Well, historically, `null` and `undefined` were internal |
| 29 | +types that needed to be converted to `any` for downstream consumers |
| 30 | +and for display. With `--strictNullChecks`, widening doesn't happen |
| 31 | +any more. But without it, widening happens a lot, generally when obtaining |
| 32 | +a type from another object. Here are some examples: |
| 33 | + |
| 34 | +```ts |
| 35 | +// @strict: false |
| 36 | +let x = null; |
| 37 | +``` |
| 38 | + |
| 39 | +Here, `null` has the type `null`, but `x` has the type `any` because |
| 40 | +of widening on assignment. `undefined` works the same way. However, |
| 41 | +with `--strict`, `null` is preserved, so no widening will happen. |
| 42 | + |
| 43 | +## Literal widening |
| 44 | + |
| 45 | +Literal widening is significantly more complex than "classic" |
| 46 | +widening. Basically, when literal widening happens, a literal type |
| 47 | +like `"foo"` or `SomeEnum.Member` gets treated as its base type: |
| 48 | +`string` or `SomeEnum`, respectively. The places where literals widen, |
| 49 | +however, cause the behaviour to be hard to understand. Literal |
| 50 | +widening is described fully |
| 51 | +[at the literal widening PR](https://github.com/Microsoft/TypeScript/pull/10676) |
| 52 | +and |
| 53 | +[its followup](https://github.com/Microsoft/TypeScript/pull/11126). |
| 54 | + |
| 55 | +### When does literal widening happen? |
| 56 | + |
| 57 | +There are two key points to understand about literal widening. |
| 58 | + |
| 59 | +1. Literal widening only happens to literal types that originate from |
| 60 | +expressions. These are called *fresh* literal types. |
| 61 | +2. Literal widening happens whenever a fresh literal type reaches a |
| 62 | +"mutable" location. |
| 63 | + |
| 64 | +For example, |
| 65 | + |
| 66 | +```ts |
| 67 | +const one = 1; // 'one' has type: 1 |
| 68 | +let num = 1; // 'num' has type: number |
| 69 | +``` |
| 70 | + |
| 71 | +Let's break the first line down: |
| 72 | + |
| 73 | +1. `1` has the fresh literal type `1`. |
| 74 | +2. `1` is assigned to `const one`, so `one: 1`. But the type `1` is still |
| 75 | +fresh! Remember that for later. |
| 76 | + |
| 77 | +Meanwhile, on the second line: |
| 78 | + |
| 79 | +1. `1` has the fresh literal type `1`. |
| 80 | +2. `1` is assigned to `let num`, a mutable location, so `num: number`. |
| 81 | + |
| 82 | +Here's where it gets confusing. Look at this: |
| 83 | + |
| 84 | +```ts |
| 85 | +const one = 1; |
| 86 | +let wat = one; // 'wat' has type: number |
| 87 | +``` |
| 88 | + |
| 89 | +The first two steps are the same as the first example. The third step |
| 90 | + |
| 91 | +1. `1` has the fresh literal type `1`. |
| 92 | +2. `1` is assigned to `const one`, so `one: 1`. |
| 93 | +3. `one` is assigned to `wat`, a mutable location, so `wat: number`. |
| 94 | + |
| 95 | +This is pretty confusing! The fresh literal type `1` makes its way |
| 96 | +*through* the assignment to `one` down to the assignment to `wat`. But |
| 97 | +if you think about it, this is what you want in a real program: |
| 98 | + |
| 99 | +```ts |
| 100 | +const start = 1001; |
| 101 | +const max = 100000; |
| 102 | +// many (thousands?) of lines later ... |
| 103 | +for (let i = start; i < max; i = i + 1) { |
| 104 | + // did I just write a for loop? |
| 105 | + // is this a C program? |
| 106 | +} |
| 107 | +``` |
| 108 | + |
| 109 | +If the type of `i` were `1001` then you couldn't write a for loop based |
| 110 | +on constants. |
| 111 | + |
| 112 | +There are other places that widen besides assignment. Basically it's |
| 113 | +anywhere that mutation could happen: |
| 114 | + |
| 115 | +```ts |
| 116 | +const nums = [1, 2, 3]; // 'nums' has type: number[] |
| 117 | +nums[0] = 101; // because Javascript arrays are always mutable |
| 118 | + |
| 119 | +const doom = { e: 1, m: 1 } |
| 120 | +doom.e = 2 // Mutable objects! We're doomed! |
| 121 | + |
| 122 | +// Dooomed! |
| 123 | +// Doomed! |
| 124 | +// -gasp- Dooooooooooooooooooooooooooooooooo- |
| 125 | +``` |
| 126 | + |
| 127 | +### What literal types widen? |
| 128 | + |
| 129 | +* Number literal types like `1` widen to `number`. |
| 130 | +* String literal types like `'hi'` widen to `string`. |
| 131 | +* Boolean literal types like `true` widen to `boolean`. |
| 132 | +* Enum members widen to their containing enum. |
| 133 | + |
| 134 | +An example of the last is: |
| 135 | + |
| 136 | +```ts |
| 137 | +enum State { |
| 138 | + Start, |
| 139 | + Expression, |
| 140 | + Term, |
| 141 | + End |
| 142 | +} |
| 143 | +const start = State.Start; |
| 144 | +let state = start; |
| 145 | +let ch = ''; |
| 146 | +while (ch = nextChar()) { |
| 147 | + switch (state) { |
| 148 | + // ... imagine your favourite tokeniser here |
| 149 | + } |
| 150 | +} |
| 151 | +``` |
| 152 | + |
| 153 | +## Narrowing |
| 154 | + |
| 155 | +Narrowing is essentially the removal of types from a union. It's |
| 156 | +happening all the time as you write code, especially if you use |
| 157 | +`--strictNullChecks`. To understand narrowing, you first need to |
| 158 | +understand the difference between "declared type" and "computed type". |
| 159 | + |
| 160 | +The declared type of a variable is the one it's declared with. For |
| 161 | +`let x: number | undefined`, that's `number | undefined`. The computed |
| 162 | +type of a variable is the type of the variable as it's used in |
| 163 | +context. Here's an example: |
| 164 | + |
| 165 | +```ts |
| 166 | +// @strict: true |
| 167 | +type Thing = { name: 'one' | 'two' }; |
| 168 | +function process(origin: Thing, extra?: Thing | undefined): void { |
| 169 | + preprocess(origin, extra); |
| 170 | + if (extra) { |
| 171 | + console.log(extra.name); |
| 172 | + if (extra.name === 'one') { |
| 173 | + // ... |
| 174 | +``` |
| 175 | +
|
| 176 | +`extra`'s declared type is `Thing | undefined`, since it's an optional |
| 177 | +parameter. However, its computed type varies based on context. On the |
| 178 | +first line, in `preprocess(origin, extra)`, its computed type is still |
| 179 | +`Thing | undefined`. However, inside the `if (extra)` block, `extra`'s |
| 180 | +computed type is now just `Thing` because it can't possibly be |
| 181 | +`undefined` due to the `if (extra)` check. Narrowing has removed |
| 182 | +`undefined` from its type. |
| 183 | +
|
| 184 | +Similarly, the declared type of `extra.name` is `'one' | 'two'`, but |
| 185 | +inside the true branch of `if (extra.name === 'one')`, its computed |
| 186 | +type is just `'one'`. |
| 187 | +
|
| 188 | +Narrowing mostly commonly removes all but one type from a union, but |
| 189 | +doesn't necessarily need to: |
| 190 | +
|
| 191 | +```ts |
| 192 | +type Type = Anonymous | Class | Interface |
| 193 | +function f(thing: string | number | boolean | object) { |
| 194 | + if (typeof thing === 'string' || typeof thing === 'number') { |
| 195 | + return lookup[thing]; |
| 196 | + } |
| 197 | + else if (typeof thing === 'boolean' && thing) { |
| 198 | + return globalCachedThing; |
| 199 | + } |
| 200 | + else { |
| 201 | + return thing; |
| 202 | + } |
| 203 | +} |
| 204 | +``` |
| 205 | +
|
| 206 | +Here, in the first if-block, `thing` narrows to `string | number` because |
| 207 | +the check allows it to be either string or number. |
| 208 | +
|
| 209 | +## Instanceof Narrowing |
| 210 | +
|
| 211 | +Instanceof narrowing looks similar to normal narrowing, and |
| 212 | +behaves similarly, but its rules are somewhat different. It only |
| 213 | +applies to certain `instanceof` checks and type predicates. |
| 214 | +
|
| 215 | +Here's a use of `instanceof` that follows the normal narrowing rules: |
| 216 | +
|
| 217 | +```ts |
| 218 | +class C { c: any } |
| 219 | +function f(x: C | string) { |
| 220 | + if (x instanceof C) { |
| 221 | + // x is C here |
| 222 | + } |
| 223 | + else { |
| 224 | + // x is string here |
| 225 | + } |
| 226 | +} |
| 227 | +``` |
| 228 | +
|
| 229 | +So far this follows the normal narrowing rules. But `instanceof` |
| 230 | +applies to subclasses too: |
| 231 | +
|
| 232 | +```ts |
| 233 | +class D extends C { d: any } |
| 234 | +function f(x: C) { |
| 235 | + if (x instanceof D) { |
| 236 | + // x is D here |
| 237 | + } |
| 238 | + else { |
| 239 | + // x is still just C here |
| 240 | + } |
| 241 | +} |
| 242 | +``` |
| 243 | +
|
| 244 | +Unlike narrowing, `instanceof` narrowing doesn't remove any types to |
| 245 | +get `x`'s computed type. It just notices that `D` is a subclass of `C` |
| 246 | +and changes the computed type to `D` inside the `if (x instanceof D)` |
| 247 | +block. In the `else` block `x` is still `C`. |
| 248 | +
|
| 249 | +If you mess up the class relationship, the compiler does its best |
| 250 | +to make sense of things: |
| 251 | +
|
| 252 | +```ts |
| 253 | +class E { e: any } // doesn't extend C! |
| 254 | +function f(x: C) { |
| 255 | + if (x instanceof E) { |
| 256 | + // x is C & E here |
| 257 | + } |
| 258 | + else { |
| 259 | + // x is still just C here |
| 260 | + } |
| 261 | +} |
| 262 | +``` |
| 263 | +
|
| 264 | +The compiler thinks that something of type `C` can't also be |
| 265 | +`instanceof E`, but just in case, it sets the computed type of `x` to |
| 266 | +`C & E`, so that you can use the properties of `E` in the block |
| 267 | +— just be aware that the block will probably never execute! |
| 268 | +
|
| 269 | +### Type predicates |
| 270 | +
|
| 271 | +Type predicates follow the same rules as `instanceof` when narrowing, |
| 272 | +and are just as subject to misuse. So this example is equivalent to |
| 273 | +the previous wonky one: |
| 274 | +
|
| 275 | +```ts |
| 276 | +function isE(e: any): e is E { |
| 277 | + return e.e; |
| 278 | +} |
| 279 | +function f(x: C) { |
| 280 | + if (isE(x)) { |
| 281 | + // x is C & E here |
| 282 | + } |
| 283 | + else { |
| 284 | + // nope, still just C |
| 285 | + } |
| 286 | +} |
| 287 | +``` |
| 288 | +
|
| 289 | +## Apparent Type |
| 290 | +
|
| 291 | +In some situations you need to get the properties on a variable, even |
| 292 | +when it technically doesn't have properties. One example is primitives: |
| 293 | +
|
| 294 | +```ts |
| 295 | +let n = 12 |
| 296 | +let s = n.toFixed() |
| 297 | +``` |
| 298 | +
|
| 299 | +`12` doesn't technically have properties; `Number` does. In order to |
| 300 | +map `number` to `Number`, we define `Number` as the *apparent type* of |
| 301 | +`number`. Whenever the compiler needs to get properties of some type, |
| 302 | +it asks for the apparent type of that type first. This applies to |
| 303 | +other non-object types like type parameters: |
| 304 | +
|
| 305 | +```ts |
| 306 | +interface Node { |
| 307 | + parent: Node; |
| 308 | + pos: number; |
| 309 | + kind: number; |
| 310 | +} |
| 311 | +function setParent<T extends Node>(node: T, parent: Node): T { |
| 312 | + node.parent = parent; |
| 313 | + return node; |
| 314 | +} |
| 315 | +``` |
| 316 | +
|
| 317 | +`T` is a type parameter, which is just a placeholder. But its |
| 318 | +constraint is `Node`, so when the compiler checks `node.parent`, it |
| 319 | +gets the apparent type of `T`, which is `Node`. Then it sees that |
| 320 | +`Node` has a `parent` property. |
0 commit comments