Last 30 Days
No notifications
Narrowing is how TypeScript turns a wide type (like string ) into a *narrower* one inside a code branch. Master
nullnumber typeof, instanceof, in, equality narrowing, custom type predicates (x is T), and assertion functions and you'll write code that's both safer and clearer.
Inside an if / switch / ?: branch, TS uses your runtime checks to *shrink* the type of a variable.
function len(x: string | string[]): number {
if (typeof x === "string") {
return x.length; // x is string here
}
return x.length; // x is string[] here
}Each kind of check unlocks a different narrowing.
typeof — primitivesfunction pad(x: string | number) {
if (typeof x === "string") return x.padStart(4, "0");
return x.toFixed(2);
}typeof returns: "string" . Note:
"function""number" "boolean" "bigint" "symbol" "undefined" "object" typeof null === "object" (legacy JS bug), so always check null separately.
function greet(name?: string) {
if (name) { // narrows away undefined AND ""
console.log(name.toUpperCase());
}
}Be careful: if (count) excludes 0 too. Use explicit != null when you only want to filter null/undefined.
if (count != null) { /* count is number, including 0 */ }function example(x: string number, y: string
boolean) {
if (x === y) {
// both are string here — only common type
x.toUpperCase();
y.toUpperCase();
}
}instanceof — classesfunction format(d: Date | string) {
if (d instanceof Date) return d.toISOString();
return d;
}in — property existencetype Cat = { meow(): void };
type Dog = { bark(): void };function speak(p: Cat | Dog) {
if ("meow" in p) p.meow();
else p.bark();
}
The cleanest form — switch on a literal tag.
type Shape =
| { kind: "circle"; r: number }
| { kind: "square"; s: number };function area(s: Shape) {
switch (s.kind) {
case "circle": return Math.PI * s.r ** 2;
case "square": return s.s ** 2;
}
}
x is TWhen the built-in checks aren't enough, write a function whose return type is a *type predicate*. Truthy return means TS narrows the argument to T.
interface Fish { swim(): void }
interface Bird { fly(): void }function isFish(p: Fish | Bird): p is Fish {
return (p as Fish).swim !== undefined;
}
function move(p: Fish | Bird) {
if (isFish(p)) p.swim();
else p.fly();
}
Use predicates for filtering arrays — TS narrows the result automatically:
const things: (Fish | Bird)[] = [/* … */];
const fishOnly = things.filter(isFish); // Fish[]> Without the predicate, filter would return (Fish | Bird)[] — TS can't see through your callback's logic.
Array methods like filter now infer narrowed types automatically when the callback returns a clearly narrowing value:
const xs = ["a", null, "b"];
const strs = xs.filter((x) => x !== null); // string[]asserts x is TLike a predicate, but for *throw-or-pass* style validators.
function assertString(x: unknown): asserts x is string {
if (typeof x !== "string") throw new Error("Expected string");
}function shout(x: unknown) {
assertString(x);
return x.toUpperCase(); // x is string from this line on
}
Node's built-in assert and Zod's .parse use this pattern.
TS tracks flow even across reassignment:
let x: string | number = 0;
x = "abc"; // narrowed to string
x.toUpperCase(); // ✅But it widens back if you reassign with the original type:
function pick(): string | number { return 1; }let v = pick();
if (typeof v === "string") {
v.toUpperCase(); // ✅
v = pick(); // back to string | number
// v.toUpperCase(); // ❌
}
! — non-null assertionA short way of telling TS "trust me, this isn't null/undefined". Use sparingly.
const root = document.getElementById("app")!; // HTMLElement, not HTMLElement nullIf you're wrong, you'll get Cannot read properties of null at runtime — TS won't save you.
Cheat sheet
Check Narrows on
typeof x === "string"primitives
x instanceof Fooclasses
"prop" in xobject structure
x === literalliteral types
Array.isArray(x)arrays
x !== nullstrips null
isFoo(x) predicateanything
assertFoo(x)anything (or throws)