Skip to content
This repository was archived by the owner on Jan 5, 2022. It is now read-only.

Commit 08f2971

Browse files
authored
Merge pull request #32 from sandersn/add-widening/narrowing
Add Widening and Narrowing
2 parents ebc9f28 + ad20dae commit 08f2971

File tree

1 file changed

+320
-0
lines changed

1 file changed

+320
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
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+
&mdash; 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

Comments
 (0)