Control flow#
Control flow in TypeScript is the JavaScript surface (see
Control flow) plus narrowing.
At every branch the compiler refines each binding’s type based
on what the condition proved. Build typed code
that exhausts every variant of a union; never is the
compiler’s signal for an unhandled case.
Narrowing#
The compiler narrows on every kind of condition.
flowchart TD
A(["x: string | number | null"]) --> B{"x == null?"}
B -->|true| C([return])
B -->|false| D{"typeof x === 'string'?"}
D -->|true| E["x: string"]
D -->|false| F["x: number"]
function display(x: string | number | null) {
if (x == null) return; // null and undefined dropped
if (typeof x === "string") {
x.toUpperCase(); // x: string
} else {
x.toFixed(2); // x: number
}
}
Narrowing dialects.
Check |
Narrows by |
|---|---|
|
primitive tag |
|
removes the literal from the union |
|
removes both |
|
to |
|
to union members that have |
|
discriminated union, to the matching member |
user-defined |
to |
Discriminated unions#
A discriminated union pairs each member with a literal tag.
switch on the tag narrows automatically.
flowchart TD
A([Shape]) --> B{s.kind}
B -->|circle| C["s: {kind:'circle', r}"]
B -->|rect| D["s: {kind:'rect', w, h}"]
C --> E[area = π·r²]
D --> F[area = w·h]
type Shape =
| {kind: "circle"; r: number}
| {kind: "rect"; w: number; h: number};
function area(s: Shape): number {
switch (s.kind) {
case "circle": return Math.PI * s.r ** 2;
case "rect": return s.w * s.h;
}
}
Go-to structure for return types that can be a success or a failure.
type Result<T, E = Error> =
| {ok: true; value: T}
| {ok: false; error: E};
function parsePort(s: string): Result<number, string> {
const n = Number(s);
if (!Number.isInteger(n) || n < 0 || n > 65535) {
return {ok: false, error: `bad port: ${s}`};
}
return {ok: true, value: n};
}
Exhaustive checking with never#
never is the empty type; nothing can be assigned to it. The
operator drops a never annotation in the default branch
of an exhaustive switch; if a new variant is added later, the
compile fails at exactly the wrong line.
function area(s: Shape): number {
switch (s.kind) {
case "circle": return Math.PI * s.r ** 2;
case "rect": return s.w * s.h;
default: {
const _exhaustive: never = s;
throw new Error(`unhandled: ${JSON.stringify(s)}`);
}
}
}
When the operator extends Shape with {kind: "tri"; ...},
_exhaustive: never = s fails to type-check because s is
now {kind: "tri"; ...}, not never.
Type guards#
A user-defined type guard is a function whose return type is
x is T. Returning true proves the narrowed type to the
compiler.
function isString(x: unknown): x is string {
return typeof x === "string";
}
function trim(x: unknown): string {
if (isString(x)) return x.trim(); // narrowed to string
throw new TypeError("expected string");
}
The asserts form throws instead of returning a boolean;
control flow past the call has the narrowed type.
function assertString(x: unknown): asserts x is string {
if (typeof x !== "string") throw new TypeError("not a string");
}
function trim2(x: unknown) {
assertString(x);
return x.trim(); // x: string from here on
}
Conditional return narrowing#
The compiler tracks narrowing across function boundaries via overloaded signatures or generic constraints.
function first<T>(xs: T[]): T | undefined;
function first<T>(xs: readonly [T, ...T[]]): T;
function first<T>(xs: T[]): T | undefined {
return xs[0];
}
const a = first([1, 2, 3]); // number | undefined
const b = first([1, 2, 3] as const); // number (non-empty tuple)
Iteration over typed collections#
for...of on a typed array carries the element type;
Object.entries widens to [string, T] (because keys are
not statically known).
const users: User[] = await loadUsers();
for (const u of users) {
u.name; // u: User
}
for (const [k, v] of Object.entries(record)) {
// k: string, v: ValueType (key is widened to string)
}
For object iteration with precise keys, use a for...in loop
plus a key cast, or write the helper with keyof.
function entries<T extends object>(obj: T): [keyof T, T[keyof T]][] {
return Object.entries(obj) as [keyof T, T[keyof T]][];
}
References#
Control flow for the underlying control-flow surface.
Types for
never, discriminated unions, andResultstructures.Functions for type guards and
assertssignatures.