Type narrowing cheatsheet.
typeof
function f(x: string | number) {
if (typeof x === "string") x.toUpperCase(); // string
else x.toFixed(); // number
}
typeof returns: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function".
instanceof
function fail(err: unknown) {
if (err instanceof Error) {
err.message;
err.stack;
}
}
Works for classes and built-ins. Doesn’t work across realms (iframes).
in operator
type Cat = { meow(): void };
type Dog = { bark(): void };
function speak(a: Cat | Dog) {
if ("bark" in a) a.bark();
else a.meow();
}
Discriminated unions
type Shape =
| { kind: "circle"; r: number }
| { kind: "square"; size: number }
| { kind: "rect"; w: number; h: number };
function area(s: Shape): number {
switch (s.kind) {
case "circle": return Math.PI * s.r ** 2;
case "square": return s.size ** 2;
case "rect": return s.w * s.h;
}
}
The literal kind field is the discriminator. TS narrows on it.
Exhaustive check (never)
function area(s: Shape): number {
switch (s.kind) {
case "circle": return Math.PI * s.r ** 2;
case "square": return s.size ** 2;
case "rect": return s.w * s.h;
default: {
const _exhaustive: never = s;
throw new Error(`unreachable: ${_exhaustive}`);
}
}
}
Adding a new variant triggers a compile error. Worth doing.
User-defined type guards
function isString(x: unknown): x is string {
return typeof x === "string";
}
function isUser(x: unknown): x is User {
return (
typeof x === "object" &&
x !== null &&
"id" in x && typeof (x as any).id === "number"
);
}
Predicate functions narrow on call.
Assertion functions
function assert(cond: unknown, msg?: string): asserts cond {
if (!cond) throw new Error(msg);
}
function assertIsString(x: unknown): asserts x is string {
if (typeof x !== "string") throw new TypeError("not string");
}
// Use
function f(x: unknown) {
assertIsString(x);
x.toUpperCase(); // x is string after assertion
}
asserts cond narrows cond; asserts x is T narrows x to T.
Truthiness narrowing
function f(s: string | null) {
if (s) s.toUpperCase(); // string (not null, not "")
}
Watch for 0, "", false if those are valid values.
Equality narrowing
function f(x: string | number, y: string | boolean) {
if (x === y) {
// both narrowed to string
x.toUpperCase();
y.toUpperCase();
}
}
Control flow narrowing
function f(x: string | null) {
if (x === null) return;
x.toUpperCase(); // x: string here
}
Early returns narrow downstream.
Type predicate vs assertion
// Predicate: returns boolean, narrows
function isUser(x: unknown): x is User { ... }
if (isUser(x)) x.id;
// Assertion: throws, narrows after call
function assertUser(x: unknown): asserts x is User { ... }
assertUser(x);
x.id;
Tagged narrowing for Promises
type Result<T> =
| { status: "ok"; value: T }
| { status: "loading" }
| { status: "error"; error: string };
function render(r: Result<string>) {
switch (r.status) {
case "ok": return r.value;
case "loading": return "...";
case "error": return r.error;
}
}
Narrowing arrays
const xs: (string | number)[] = [];
if (xs.every(x => typeof x === "string")) {
// xs still (string | number)[] — `every` doesn't narrow
}
// Use a type predicate:
function allStrings(arr: unknown[]): arr is string[] {
return arr.every(x => typeof x === "string");
}
if (allStrings(xs)) {
xs.map(s => s.toUpperCase());
}
Object property narrowing
function f(x: { a?: string }) {
if (x.a) x.a.toUpperCase(); // narrowed
// But function call resets:
function inner() { x.a.toUpperCase(); } // ERROR
}
Reassign locally:
const a = x.a;
if (a) {
function inner() { a.toUpperCase(); } // ok
}
Const assertion + narrowing
function f(x: "a" | "b") { ... }
const k = "a"; // type: "a"
f(k); // ok
let k2 = "a"; // type: string — no good!
f(k2); // ERROR
let k3 = "a" as const; // type: "a"
f(k3); // ok
Branded types (nominal)
type UserId = string & { __brand: "UserId" };
function makeUserId(s: string): UserId {
return s as UserId;
}
function getUser(id: UserId) { ... }
const raw = "abc";
getUser(raw); // ERROR
getUser(makeUserId(raw)); // ok
Forces explicit construction.
Common mistakes
typeof x === "object"doesn’t exclude null (typeof null === “object”).- Forgetting exhaustive check — silent breakage when union grows.
- Type guard without proper runtime check — lying to the compiler.
innarrows but doesn’t guarantee the value type — still need to validate.- Reassigning narrowed values — narrowing is lost.
Read this next
If you want my type-guard library, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .