Type Guards & Narrowing
Narrowing types with guards, predicates, and assertion functions
Type narrowing refines a broad type to a more specific one within a code block.
What you'll learn
Use typeof, instanceof, and in operators for runtime checks
Write custom type predicate functions (is)
Narrow discriminated unions with switch and if
Use assertion functions to narrow after validation
Apply never for exhaustiveness checking
Build reusable custom type guards
Built-in Guards: typeof, instanceof, in
// typeof — narrows primitives
function format(input: string | number): string {
if (typeof input === "string") return input.trim()
return input.toFixed(2)
}
// instanceof — narrows class instances
function toJSON(data: Date | Error): string {
if (data instanceof Date) return data.toISOString()
return data.message
}
// in — narrows object shapes
type Cat = { meow: () => void }
type Dog = { bark: () => void }
function speak(animal: Cat | Dog): void {
if ("meow" in animal) animal.meow()
else animal.bark()
}
Type Predicates (is)
Custom functions that tell TypeScript what a value is.
interface User {
name: string
email: string
}
interface Admin extends User {
role: "admin"
permissions: string[]
}
function isAdmin(user: User): user is Admin {
return "role" in user && user.role === "admin"
}
function handleUser(user: User) {
if (isAdmin(user)) {
// TypeScript knows user is Admin here
console.log(user.permissions)
}
}
Nested Narrowing
type APIResponse =
| { status: 200; data: unknown }
| { status: 201; data: unknown; location: string }
| { status: 400; error: string }
| { status: 500; error: string; retryAfter?: number }
function handleResponse(res: APIResponse): string {
if (res.status === 200 || res.status === 201) {
// Narrowed to success variants
return `Data: ${JSON.stringify(res.data)}`
}
// Narrowed to error variants (400 | 500)
return `Error: ${res.error}`
}
Tip
Use switch on a discriminant property — TypeScript narrows per case. Add a
default: assertNever(x) to catch unhandled variants.
Assertion Functions
Throw-based narrowing for validation flows.
function assertString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error("Expected string, got " + typeof value)
}
}
function process(input: unknown) {
assertString(input)
// input is string here
console.log(input.toUpperCase())
}
// Asserting object shape
interface Config {
endpoint: string
timeout: number
}
function assertConfig(obj: unknown): asserts obj is Config {
if (typeof obj !== "object" || obj === null) throw new Error("Not object")
if (!("endpoint" in obj) || typeof (obj as any).endpoint !== "string")
throw new Error("Missing endpoint")
if (!("timeout" in obj) || typeof (obj as any).timeout !== "number")
throw new Error("Missing timeout")
}
Custom Type Guard Patterns
// Guard for non-null array items
function isDefined<T>(value: T | null | undefined): value is NonNullable<T> {
return value !== null && value !== undefined
}
const items = [1, null, 2, undefined, 3].filter(isDefined)
// items: number[]
// Guard for string arrays
function isStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every((v) => typeof v === "string")
}
Exhaustiveness with never
type Event =
| { type: "click"; x: number; y: number }
| { type: "keydown"; key: string }
| { type: "focus" }
function handleEvent(event: Event): string {
switch (event.type) {
case "click":
return `Click at (${event.x}, ${event.y})`
case "keydown":
return `Key pressed: ${event.key}`
case "focus":
return "Element focused"
default:
return assertNever(event)
}
}
function assertNever(value: never): never {
throw new Error("Unreachable: " + value)
}
// Adding { type: "blur" } to Event causes compile error here
Type-Safe Event Handler
Code
typescript
1type UIEvent =2| { kind: "click"; button: "left" | "right" }3| { kind: "hover"; x: number; y: number }4| { kind: "scroll"; deltaX: number; deltaY: number }5 6function handleUIEvent(event: UIEvent): string {7// Use switch on event.kind and return a description for each variant8}Key Takeaways
typeof narrows primitives, instanceof narrows classes, in narrows object shapes.
Type predicates (x is T) let you write reusable custom narrowing functions.
Discriminated unions narrow cleanly with switch on the discriminant.
Assertion functions (asserts x is T) throw on invalid input and narrow after return.
never + exhaustiveness checking catches missing branches at compile time.
Custom guards like isDefined compose well with Array.filter for type-safe collections.