Narrowing in TypeScript
Narrowing refines a union to a specific constituent via checks that the compiler understands.
Equality and truthiness
function f(x: number | null | undefined) {
if (x == null) return 0; // null or undefined
return x + 1; // number
}
function g(s: string | "") {
if (!s) return 0; // falsy narrows to ""
return s.length; // string
}
Prefer ?? for defaulting: x ?? 0.
typeof / instanceof / in
function id(x: string | number) {
return typeof x === "number" ? x : Number(x);
}
class A {}
function h(v: A | Date) {
if (v instanceof Date) return v.getTime();
return v; // A
}
function hasName(x: unknown) {
if (typeof x === "object" && x && "name" in x) {
// x has name
}
}
Discriminated unions
type Msg = { type: "text"; text: string } | { type: "img"; url: string };
function render(m: Msg) {
if (m.type === "text") return m.text; // Msg -> text
return `<img src="${m.url}">`; // Msg -> img
}
User-defined guards and asserts
function isStr(x: unknown): x is string { return typeof x === "string" }
function assertStr(x: unknown): asserts x is string { if (!isStr(x)) throw new Error("!string") }
Exhaustive checks
function exhaustive(x: never): never { throw new Error(String(x)) }
Use in switch default to ensure all cases are handled.
Summary
- Combine checks and control flow; the compiler follows
- Prefer discriminants and user-defined guards for clarity
- Enforce exhaustiveness with
never