Overview
Control-flow analysis narrows a variable to a more specific type inside a branch; prefer it to casting. The rationale is in typescript-narrowing. Snippets compile under strict.
Built-in guards
Narrow with the built-in guard that matches the check, not with a cast.
| Guard | Narrows to |
|---|---|
typeof x === "string" ("number", "boolean", "bigint", "symbol", "undefined", "function") | That primitive or function type. |
x === null, x == null | null; == null covers null and undefined. |
if (x) | Removes null, undefined, 0, "", false. |
err instanceof TypeError | The class instance type. |
"name" in obj | The union members that have name. |
Array.isArray(x) | An array type. |
a?.b in a condition | Removes null and undefined from a in the true branch. |
function show(v: string | number | null) {
if (v === null) return;
return typeof v === "string" ? v.length : v.toFixed(2); // string | number narrowed
}Type predicates
A function returning v is T narrows its argument at the call site.
function isCat(a: Cat | Dog): a is Cat { return "meow" in a; }
const present = [1, undefined, 2].filter((x) => x !== undefined); // number[] (TypeScript 5.5+ infers the predicate)Before 5.5, write the predicate by hand: .filter((x): x is number => x !== undefined). The compiler does not check that a handwritten predicate is truthful.
Discriminated unions
Give every variant a literal field (type, kind) and switch on it.
type Shape =
| { type: "circle"; radius: number }
| { type: "rect"; width: number; height: number };
function assertNever(x: never): never { throw new Error(`Unhandled: ${JSON.stringify(x)}`); }
function area(s: Shape): number {
switch (s.type) {
case "circle": return Math.PI * s.radius ** 2;
case "rect": return s.width * s.height;
default: return assertNever(s); // compile error when a variant is missing
}
}Assertion functions
Use assertions at trust boundaries, and validate real data with a schema.
| Signature | Effect after the call |
|---|---|
function assert(cond: unknown): asserts cond | cond is truthy. |
function assertDefined<T>(v: T | null | undefined): asserts v is T | v is not nullish. |
declare const user: { name: string } | null;
assertDefined(user);
user.name; // narrowed to { name: string }Gotchas
Avoid these; each hides an unsound narrowing.
| Gotcha | Fix |
|---|---|
typeof null === "object". | Check x !== null before using an object branch. |
| A wrong handwritten predicate or assertion silently misleads the compiler. | Keep them small, and validate untrusted data with a schema (typescript-runtime-validation). |
"k" in x throws at runtime when x is a primitive or nullish. | Guard x first. |
A discriminant typed string or number narrows nothing. | Use literal types. |
assertNever catches missing variants only at compile time. | At runtime it is your guard against unexpected external data. |