Type Narrowing
Similar to type widening, type narrowing is a related concept that demonstrates TypeScript’s pragmatism and usefulness. Read the code below and see if you can identify any issues.
function padLeft(padding: number | string, input: string) {
if (typeof padding === "number") {
return " ".repeat(padding) + input;
}
return padding + input;
}
In the above TypeScript function, you may expect the code inside the if
throw an error because " ".repeat(count: number)
expects a mumber
type as its parameter; however, the variable padding
is of a union type number|string
. How does it work? Under the hood of this seemingly uninteresting example, a lot of magic is happening. TypeScript uses specific checks for various code execution paths (if/else
, for
, in
) and makes intelligent type inference decisions. It recognises the typeof
operator as one of the type guards, and is confident that after the typeof padding === "number"
check, the variable will indeed be of number
type, allowing the code to pass the check.
const
assertions
Apart from the no-widening literal types demonstrated in the Type Widening example, TypeScript 3.4 introduced a new construct for literal values called const
assertions. It uses a type assertion in place of the type name. For example:
// Type '"hello"', no literal widening from "hello" to "string"
let x = "hello" as const;
// Type 'readonly [10, 20]', turning Array into a tuple
let y = [10, 20] as const;
// Type '{ readonly text: "hello" }', object literal gets readonly properties
let z = { text: "hello" } as const;
// It is possible to use angle bracket assertion syntax
// outside of `.tsx` files,
// Type '"hello"
let x = <const>"hello";
// Type 'readonly [10, 20]'
let y = <const>[10, 20];
// Type '{ readonly text: "hello" }'
let z = <const>{ text: "hello" }; `
This feature helps TypeScript to narrow types without explicit annotations.
// Works with no types referenced or declared.
// We only needed a single const assertion.
function getShapes() {
let result = [
{ kind: "circle", radius: 100 },
{ kind: "square", sideLength: 50 },
] as const;
return result;
}
for (const shape of getShapes()) {
// Narrows perfectly!
if (shape.kind === "circle") {
console.log("Circle radius", shape.radius);
} else {
console.log("Square side length", shape.sideLength);
}
}
Note that const
assertion can only be applied immediately after a single type literal. It won’t work with expressions:
// Error! A 'const' assertion can only be applied to a
// to a string, number, boolean, array, or object literal.
let a = (Math.random() < 0.5 ? 0 : 1) as const;
let b = (60 * 60 * 1000) as const;
// Works!
let c = Math.random() < 0.5 ? (0 as const) : (1 as const);
let d = 3_600_000 as const;
Similar to declaring a variable with const
, it only makes the variable un-reassignable and does not make it immutable.
let arr = [1, 2, 3, 4];
let foo = {
name: "foo",
contents: arr,
} as const;
foo.name = "bar"; // error!
foo.contents = []; // error!
foo.contents.push(5); // ...works!
Flow-based type inference
TypeScript uses flow-based type inference, which means TS will ‘read’ the code flow as humans would and reason the code based on flow control and operators such as if/else
, switch
, in
, typeof
||
etc., to refine the types as it walks down through the code. Flow-based type inference also exists in Flow, Kotlin, and Ceylon. It refines types within a block of code, while many static-typed languages, such as C++ and Java, use symbols to determine the types of variables.
The blow example demonstrates how TS narrows (refines) the type of the width
variable as it ‘reads’ the code:
// We use a union of string literals to describe
// the possible values a CSS unit can have
type Unit = 'cm' | 'px' | '%';
// Enumerate the units
const units: Unit[] = ['cm', 'px', '%'];
// Check the unit, and return if there is no match
function parseUnit(value: string): Unit | null {
for (let i = 0; i < units.length; i++) {
if (value.endsWith(units[i])) {
return units[i];
}
}
return null;
}
type Width {
unit: Unit,
value: number;
} // Representing a CSS width type
function parseWidth(width: number | string | null | undefined ): Width | null {
// If width is null or undefined, return early
if (width == null) {
return null;
}
// If width is a number, default to pixels
if (typeof width === 'number') {
// TS: (parameter) width: string | number
// TypeScript has now refined the type of `width` to `string | number`,
// as if knows that `width` can not be a `null` anymore 😳
return { unit: 'px', value: width };
}
// Try to parse a unit from a width
let unit = parseUnit(width);
// TS: (parameter) width: string
// TS believes that `width` can only be a string now
if (unit) {
return { unit, value: parseFloat(width) };
}
// Otherwise, return `null`
return null;
}
Discriminated Union Types
Flow-based type inference can narrow simple union types effectively. However, when there are chances of overlap of properties among union type members (by definition of union, the new type can ‘pick-and-choose’ from any of properties from any of the union type members), TypeScript must check each property of the type in question to narrow it down to a known union member safely.
A common solution is to use a discriminated union types, also called sum types in other languages. You turn a regular union type into a discriminated union types by include a property of string literal tpye in all members of the union type, such as state
or type
.
type UserTextEvent = {
type: 'TextEvent',
value: string,
target: HTMLInputElement
}
type UserMouseEvent = {
type: 'MouseEvent',
value: [number, number],
target: HTMLElement
}
type UserEvent = UserTextEvent | UserMouseEvent
function handle(event: UserEvent) {
if (event.type === 'TextEvent') {
event.value // string
event.target // HTMLInputElement,
// TS can safely assume that its a UserTextEvent because of the check
// on the common property `type`
// ...
return;
}
event.value // [number, number] event.target // HTMLElement
}
References
- www.typescriptlang.org. (n.d.). Documentation - Narrowing. [online] Available at: https://www.typescriptlang.org/docs/handbook/2/narrowing.html [Accessed 2 Apr. 2023].
- www.typescriptlang.org. (n.d.). Documentation - TypeScript 3.4. [online] Available at: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions [Accessed 2 Apr. 2023].