TypeScript Narrowing

Photo by Numeroo77 on Unsplash

Note that some examples are extracted from the TypeScript Handbook, and all tributes should go to it.

Why need narrowing

Let’s say we have a union type, TypeScript will only allow you to do things with the union if the thing is valid for every member of the union due to contextual typing:

function printId(id: number | string) {console.log(id.toUpperCase());
// Property 'toUpperCase' does not exist on type 'string | number'.

The solution is to narrow the union with code, the same as in JavaScript with if else statement. Narrowing occurs when TypeScript can deduce a more specific type for a value based on the context of the code:

function printId(id: number | string) {if (typeof id === "string") {
console.log(id.toUpperCase()); // id: string
} else {
console.log(id); // id: number
}
}

typeof type guards

As we’ve seen, JavaScript supports a typeof operator which can give very basic information about the type of values we have at runtime. TypeScript expects this to return a certain set of strings:

"string", "number", "bigint", "boolean", "symbol", "undefined", "object", "function"

Note that the discrepancy between type definition in JS and TS can cause problem, e.g. in JavaScript, typeof null is actually "object".

Truthiness narrowing

function printAll(strs: string | string[] | null) {
if (strs && typeof strs === "object") {
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. For example:

function example(x: string | number, y: string | boolean) {
if (x === y) {
x.toUpperCase();
y.toLowerCase();
// x, y : string
} else {
console.log(x);
// x: string | number
console.log(y);
// y: string | boolean
}
}

Note that == null actually also checks whether it’s undefined or null. The same applies to == undefined.

interface Container {
value: number | null | undefined;
}
function multiplyValue(container: Container, factor: number) {
// Remove both 'null' and 'undefined' from the type.
if (container.value != null) {
console.log(container.value);
}
}

instanceof narrowing

function logValue(x: Date | string) {
if (x instanceof Date) {
console.log(x.toUTCString());
// x: Date
} else {
console.log(x.toUpperCase());
// x: string
}
}

Assignments

As we mentioned earlier, when we assign to any variable, TypeScript looks at the right side of the assignment and narrows the left side appropriately.

Control flow analysis

In the process of narrowing, TypeScript can analyse the code and see the part of the code that is unreachable, and deduce the type accordingly. This analysis based on reachability is control flow analysis. It is used when TS encounters type guards and assignments. When a variable is analysed, control flow can split off and re-merge again, and that variable can be observed to have a different type at each point.

function example() { let x: string | number | boolean; x = Math.random() < 0.5;
console.log(x);
// x: boolean
if (Math.random() < 0.5) {
x = "hello";
console.log(x);
// x: string
} else {
x = 100;
console.log(x);
// x: number
}
return x;
// x: string | number | boolean;
}

Type predicates

To define a user-defined type guard, we simply need to define a function whose return type is a type predicate:

type Fish = { swim: () => void };
type Bird = { fly: () => void };
declare function getSmallPet(): Fish | Bird;function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}

pet is Fish is our type predicate in this example. A predicate takes the form parameterName is Type, where parameterName must be the name of a parameter from the current function signature.

Any time isFish is called with some variable, TypeScript will narrow that variable to that specific type if the original type is compatible.

// Both calls to 'swim' and 'fly' are now okay.
let pet = getSmallPet();
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}

Discriminated Union

Discriminated Unions is a pattern to build types that shares a common property but have different result depends on the condition:

  • Types that have a common type property — the discriminant.
  • A type alias that takes the union of those types — the union.
  • A type guard on the common property (on the discriminant)

If you have a class with a literal member then you can use that property to discriminate between union members. If you use a type guard style check (==, ===, !=, !==) or switch on the discriminant property.

For example, we want to calculate the area based on different objects:

interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
radius: number;
}
type Shape = Square | Rectangle | Circle;function area(s: Shape) {
if (s.kind === “square”) {
return s.size * s.size;
}
else if (s.kind === “rectangle”) {
return s.width * s.height;
}
else {
const _exhaustiveCheck: never = s;
//
Argument of type 'Circle' is not assignable to parameter of type 'never'.
}
}

Note the last fall through is to make sure that all types — including the unexpected ones (in our case, the Circle) have some action against them. (exhaustive check).

The shortened version using Switch statement:

function area(s: Shape) {
switch (s.kind) {
case “square”: return s.size * s.size;
case “rectangle”: return s.width * s.height;
case “circle”: return Math.PI * s.radius * s.radius;
default: const _exhaustiveCheck: never = s;
return _exhaustiveCheck;
}
}

We can convert the exhaustive check to use a function:

function assertNever(x:never): never {
throw new Error('Unexpected value. Should have been never.');
}
function area(s: Shape) {
switch (s.kind) {
case “square”: return s.size * s.size;
case “rectangle”: return s.width * s.height;
default: return assertNever(s);
}
}

That’s so much of it. Please refer to the Handbook for more details.

Happy Reading.

--

--

--

Hi :)

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

TestMace beta. Features overview, part 1

Notifications In Flutter With Nodejs Server Using Firebase Cloud Messaging.

Strategy Pattern Implementation with Typescript and Angular

Basic Conditional Rendering In React Using the Logical && Operator

Angular Communication between components

Class #4 Austin Coding Academy

How to set up ReactJS, Redux-Saga and Passport.js

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
E.Y.

E.Y.

Hi :)

More from Medium

Typescript Loop in Certain Time ⌚

Typescript: A Review On Decorators

Generate TypeScript Declaration Files for JavaScript Files

The JavaScript logo with an arrow pointing towards the TypeScript logo

TypeScript Quiz_type aliases, union type, intersection type, enum