Narrowing Flashcards
What is narrowing?
The process of refining types to more specific types than declared is called narrowing.
TypeScript follows possible paths of execution that our programs can take to analyze the most specific possible type of a value at a given position. It looks at these special checks (called type guards) and assignments.
Source typescriptlang
typeof
type guards
In TypeScript, checking against the value returned by typeof
is a type guard. Because TypeScript encodes how typeof
operates on different values.
Source typescriptlang
Explain truthiness narrowing ?
In JavaScript, we can use any expression in conditionals, &&
s, ||
s, if
statements, Boolean negations (!
), and more. Constructs like if
first “coerce” their conditions to boolean
s to make sense of them, and then choose their branches depending on whether the result is true
or false
.
Values like 0
, NaN
, ""
(the empty string), 0n
(the bigint version of zero), null
, undefined
all coerce to false
, and other values get coerced true
. You can always coerce values to boolean
s by running them through the Boolean
function, or by using the shorter double-Boolean negation (!!
). (The latter has the advantage that TypeScript infers a narrow literal boolean
type true
, while inferring the first as type boolean
.)
// both of these result in 'true' Boolean("hello"); // type: boolean, value: true !!"world"; // type: true, value: true
Sources typescriptlang
Explain equality narrowing
TypeScript uses switch
statements and equality checks like ===
, !==
, ==
, and !=
to narrow types.
Source typescriptlang
Explain in
operator narrowing
JavaScript has an operator for determining if an object has a property with a name, the in
operator. TypeScript takes this into account as a way to narrow down potential types.
For example:
type Fish = { swim: () => void }; type Bird = { fly: () => void }; function move(animal: Fish | Bird) { if ("swim" in animal) { // (parameter) animal: Fish return animal.swim(); } return animal.fly(); // (parameter) animal: Bird }
Source typescriptlang
Explain instanceof
narrowing
JavaScript has an operator for checking whether or not a value is an “instance” of another value. More specifically, in JavaScript x instanceof Foo
checks whether the prototype chain of x
contains Foo.prototype
.
TypeScript uses instanceof
as a type guard, and narrows types in branches guarded by instanceof
s.
Source typescriptlang
Explain assignment
narrowing
TypeScript looks at the right side of the assignment and narrows the left side appropriately.
let x = Math.random() < 0.5 ? 10 : "hello world!"; // let x: string | number x = 1; console.log(x); // let x: number x = "goodbye!"; console.log(x); // let x: string
Source typescriptlang
Explain control flow analysis
TypeScript not only narrows looking for type guards in if
s, while
s, conditionals, etc. It also does an analysis of code based on reachability called control flow analysis.
TypeScript uses this flow analysis to narrow types as it encounters type guards and assignments. When a variable is analyzed, control flow can split off and re-merge over and over again, and that variable can be observed to have a different type at each point. For example:
function padLeft(padding: number | string, input: string) { if (typeof padding === "number") { return " ".repeat(padding) + input; } return padding + input; }
padLeft
returns from within its first if
block. TypeScript was able to analyze this code and see that the rest of the body (return padding + input;
) is unreachable in the case where padding
is a number
. As a result, it was able to remove number
from the type of padding
(narrowing from string | number
to string
) for the rest of the function.
Source typescriptlang
What is a type predicate
?
A type predicate
is a user-defined type guard which takes the form of parameterName is Type
, where parameterName must be the name of a parameter from the current function signature:
function isFish(pet: Fish | Bird): pet is Fish { return (pet as Fish).swim !== undefined; }
pet is Fish
is our type predicate in this example. Any time isFish
is called with some variable, TypeScript will narrow that variable to that specific type if the original type is compatible.
Source typescriptlang
Explain discriminated unions
A discriminated union
is a union of two or more interface that share a common discrimated property. For example instead of:
interface Shape { kind: "circle" | "square"; radius?: number; sideLength?: number; }
You should consider creating a discriminated function like the following:
interface Circle { kind: "circle"; radius: number; } interface Square { kind: "square"; sideLength: number; } type Shape = Circle | Square;
In the example kind
is the property used to discriminate against the different possible objects. If you filter by kind === 'circle'
you won’t need to do a type assertion as
to use the radious
as you would have needed with the initial interface.
Source typescriptlang
Explain the never
type
When narrowing, you can reduce the options of a union to a point where you have removed all possibilities and have nothing left. In those cases, TypeScript will use a never
type to represent a state which shouldn’t exist.
Source typescriptlang
Explain exhaustiveness checking
The never
type is assignable to every type; however, no type is assignable to never
(except never
itself). This means you can use narrowing and rely on never
turning up to do exhaustive checking in a switch statement.
For example, adding a default to our getArea
function which tries to assign the shape to never
will raise when every possible case has not been handled.
type Shape = Circle | Square; function getArea(shape: Shape) { switch (shape.kind) { case "circle": return Math.PI * shape.radius ** 2; case "square": return shape.sideLength ** 2; default: const _exhaustiveCheck: never = shape; return _exhaustiveCheck; } }
Adding a new member to the Shape union, will cause a TypeScript error:
interface Triangle { kind: "triangle"; sideLength: number; } type Shape = Circle | Square | Triangle; function getArea(shape: Shape) { switch (shape.kind) { case "circle": return Math.PI * shape.radius ** 2; case "square": return shape.sideLength ** 2; default: const _exhaustiveCheck: never = shape; // Error: Type 'Triangle' is not assignable to type 'never'. return _exhaustiveCheck; } }
Source typescriptlang