Overview
Narrowing turns a wide type into a usable one inside a branch. The compiler narrows on guards, discriminants, and assertion functions; everything else is a cast. Strict mode makes wide types unsafe to use, which is why narrowing matters (typescript-strict-mode). For a lookup table of patterns, see typescript-narrowing-patterns.
Use typeof for primitives and instanceof for classes
typeof returns one of eight strings: "string", "number", "bigint", "boolean", "symbol", "undefined", "object", "function". typeof null is "object", so rule out null with value != null or === null.
instanceof narrows to a class instance and is the usual guard after catch (the variable is unknown). It fails across realms (iframes, vm contexts) and on plain object literals; use a discriminant for those.
try {
await loadInvoice(id)
} catch (err) {
if (err instanceof HttpError) return { status: err.status }
throw err
}Use in only for two shapes without a tag
"meow" in animal narrows Cat | Dog when only one has the property. With three or more shapes, add a discriminant field instead.
Prefer discriminated unions
A literal-typed tag (kind, type, status) narrows by equality, and each case sees exactly its variant. End the switch with a never assignment so adding a variant breaks every switch that forgot it.
function label(inv: Invoice): string {
switch (inv.status) {
case "draft": return "Draft"
case "sent": return `Sent ${inv.sentAt.toISOString()}`
case "paid": return `Paid ${inv.paidAt.toISOString()}`
default: {
const unhandled: never = inv
throw new Error(`Unhandled: ${JSON.stringify(unhandled)}`)
}
}
}Let inference write simple predicates
Since TypeScript 5.5 the compiler infers a type predicate from a function that returns a boolean check, so .filter narrows without an annotation.
const defined = items.filter((x) => x !== undefined) // T[] in 5.5+, (T | undefined)[] beforeWrite value is T yourself only when the check is not a simple comparison, and keep the body honest: the compiler trusts it. For untrusted input (HTTP, files, env) a predicate that checks two fields is not validation; use a schema (typescript-runtime-validation).
function isInvoice(v: unknown): v is Invoice {
return typeof v === "object" && v !== null && "status" in v
}Use assertion functions for invariants
asserts value is T throws on failure and narrows after the call. Use it where you know something the type system does not, such as the first row of a non-empty result under noUncheckedIndexedAccess.
function assertDefined<T>(v: T | undefined, msg: string): asserts v is T {
if (v === undefined) throw new Error(msg)
}Return early instead of re-checking
After if (value === null) return "", the rest of the function already sees string. Re-checking !== null inside makes the reader prove the narrowing twice. Use NonNullable<T> (typescript-utility-types) when you need to name the narrowed type.