Typescript - Narrowing
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, and the process of refining types to more specific types than declared is called narrowing. In many editors we can observe these types as they change, and we’ll even do so in our examples.
function padLeft(padding: number | string, input: string) {
if (typeof padding === "number") { // type gard
return " ".repeat(padding) + input; // narrowing
}
return padding + input;
}
Type guard
typeOf narrowing
- ex:
string,number,object,undefined... nullis alsoobject(example as below)
function printAll(strs: string | string[] | null) {
if (typeof strs === "object") {
for (const s of strs) { // 'strs' is possibly 'null'.
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
} else {
// do nothing
}
}
Truthiness narrowing
In JavaScript, we can use any expression in conditionals, &&s, ||s, if statements, Boolean negations (!), and more.
function printAll(strs: string | string[] | null) {
if (strs && typeof strs === "object") { // exclude null and undefined
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
}
}
Equality narrowing
TypeScript also uses switch statements and equality checks like ===, !==, ==, and != to narrow types.
== nulland== undefinedcheck whether a value is eithernullorundefined
The in operator narrowing
JavaScript has an operator for determining if an object or its prototype chain has a property with a name: the in operator.
instanceof narrowing
JavaScript has an operator for checking whether or not a value is an “instance” of another value. (x instanceof Foo checks whether the prototype chain of x contains Foo.prototype)
Using type predicates
We’ve worked with existing JavaScript constructs to handle narrowing so far, however sometimes you want more direct control over how types change throughout your code.
To define a user-defined type guard, we simply need to define a function whose return type is a type predicate:
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
Assertion functions
There’s a specific set of functions that throw an error if something unexpected happened. They’re called “assertion” functions.
function multiply(x, y) {
assert(typeof x === "number");
assert(typeof y === "number");
return x * y;
}
Discriminated unions
For dealing with complex structures.
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
(parameter) shape: Circle
case "square":
return shape.sideLength ** 2;
(parameter) shape: Square
}
}
The never type
TypeScript will use a never type to represent a state which shouldn’t exist.
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.
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;
}
}