TypeScript Type Guards Cheat Sheet
Covers narrowing with typeof, instanceof, the in operator, custom type predicates, and assertion functions for safer runtime checks.
typeof & instanceof Guards
Built-in narrowing for primitives and class instances.
function format(value: string | number) { if (typeof value === 'string') { return value.toUpperCase(); // narrowed to string } return value.toFixed(2); // narrowed to number}class Dog { bark() {} }class Cat { meow() {} }function speak(animal: Dog | Cat) { if (animal instanceof Dog) { animal.bark(); // narrowed to Dog } else { animal.meow(); // narrowed to Cat }}
Custom Type Predicates
Write reusable functions that narrow a union with 'is'.
interface Fish { swim(): void; }interface Bird { fly(): void; }function isFish(pet: Fish | Bird): pet is Fish { // type predicate return (pet as Fish).swim !== undefined;}function move(pet: Fish | Bird) { if (isFish(pet)) { pet.swim(); // narrowed to Fish } else { pet.fly(); // narrowed to Bird }}
'in' Operator & Discriminated Unions
Narrow by property presence or by a shared literal discriminant.
interface Admin { role: 'admin'; permissions: string[]; }interface Member { role: 'member'; }function describe(user: Admin | Member) { if ('permissions' in user) { // 'in' narrows by property presence console.log(user.permissions); } if (user.role === 'admin') { // discriminant narrowing console.log(user.permissions); }}
Assertion Functions (TS 3.7+)
Throw-based narrowing that persists after the function call returns.
function assertIsString(val: unknown): asserts val is string { if (typeof val !== 'string') { throw new Error('Expected a string'); }}function process(input: unknown) { assertIsString(input); input.toUpperCase(); // narrowed to string after the assertion call}function assertDefined<T>(val: T): asserts val is NonNullable<T> { if (val === null || val === undefined) throw new Error('Value is required');}
Narrowing Techniques
The full toolbox TypeScript offers for narrowing a type.
- typeof guard- Narrows primitives: 'string' | 'number' | 'boolean' | 'undefined' | 'function' | 'object' | 'symbol' | 'bigint'
- instanceof guard- Narrows by prototype chain for class instances
- in operator- Narrows by checking whether a property exists on the object
- user-defined type predicate- function isX(v): v is X { ... }
- assertion functions- asserts val is T throws instead of returning a boolean
- discriminated union narrowing- Switching/branching on a shared literal property like 'kind' or 'type'
- Array.isArray()- Built-in guard that narrows unknown to an array type
- truthiness narrowing- if (value) removes null/undefined/''/0 from the type
Narrowing Arrays with filter()
A type predicate passed to Array.prototype.filter narrows the resulting array's element type.
interface Task { id: string; dueDate: Date | null; }function isScheduled(t: Task): t is Task & { dueDate: Date } { return t.dueDate !== null;}const tasks: Task[] = [{ id: '1', dueDate: null }, { id: '2', dueDate: new Date() }];const scheduled = tasks.filter(isScheduled); // (Task & { dueDate: Date })[]scheduled[0].dueDate.getTime(); // no null check needed// Without the predicate, filter(t => t.dueDate !== null) still returns Task[]
Branded (Nominal) Types with a Guard
Simulate nominal typing over structurally-identical primitives, validated by a runtime guard.
type Brand<T, B extends string> = T & { readonly __brand: B };type Email = Brand<string, 'Email'>;function isEmail(value: string): value is Email { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);}function sendTo(email: Email) { /* ... */ }const raw = '[email protected]';if (isEmail(raw)) { sendTo(raw); // raw is narrowed to Email, plain strings are rejected at the call site}
Narrowing Lost Inside Callbacks
TypeScript can't guarantee a mutable outer variable is unchanged by the time a callback runs, so narrowing resets.
function process(value: string | null) { if (value === null) return; // value: string here setTimeout(() => { value.toUpperCase(); // Error: value could be reassigned to null before the callback runs }, 100); // fix: capture a local const, which cannot be reassigned const safeValue = value; setTimeout(() => { safeValue.toUpperCase(); // OK - const can't change }, 100);}
Generic Type Guard Factories
Build reusable, parameterized predicates instead of hand-writing one guard per type.
function isInstanceOf<T>(ctor: new (...args: any[]) => T) { return (value: unknown): value is T => value instanceof ctor;}class ApiError extends Error {}const isApiError = isInstanceOf(ApiError);function handle(err: unknown) { if (isApiError(err)) { console.log(err.message); // narrowed to ApiError }}// Generic property-presence guardfunction hasKey<K extends string>(obj: object, key: K): obj is Record<K, unknown> { return key in obj;}
Type Guard Gotchas
Subtle rules that affect whether narrowing actually applies.
- Guards don't cross function boundaries- Calling a plain boolean-returning helper (not typed with 'is') never narrows the caller's type, even if the logic is correct
- 'this' type predicates- Methods can narrow 'this': `isAdmin(this: User): this is Admin` for fluent, class-based narrowing
- Guards on readonly/mutable mismatch- A predicate on a mutable type does not automatically apply if the value is later widened to readonly, and vice versa
- Discriminant must be a literal- 'in' and equality narrowing only work reliably when the discriminant property is a literal type, not a wide string/number
- Array.isArray with tuples- Array.isArray narrows 'unknown' to 'any[]', not to a specific tuple shape - combine with length/element checks for tuples
- Guard return type omission- Omitting the 'is' clause and just returning boolean compiles fine but silently disables narrowing at every call site
Type predicates (pet is Fish) are only as safe as the logic inside the function - TypeScript trusts your implementation completely and won't verify it, so a buggy type guard can silently produce unsound narrowing.