Back to Blog
TypeScriptType GuardsType SystemPatterns

TypeScript Type Guards and Narrowing

Master type narrowing in TypeScript. From typeof to instanceof to custom type guards.

B
Bootspring Team
Engineering
July 19, 2021
6 min read

Type guards narrow types at runtime. Here's how to use them effectively.

typeof Guard#

1function processValue(value: string | number) { 2 if (typeof value === 'string') { 3 // TypeScript knows value is string 4 return value.toUpperCase(); 5 } 6 // TypeScript knows value is number 7 return value.toFixed(2); 8} 9 10// typeof works for primitives 11function handle(input: string | number | boolean | undefined) { 12 if (typeof input === 'string') { 13 return input.trim(); 14 } 15 if (typeof input === 'number') { 16 return input * 2; 17 } 18 if (typeof input === 'boolean') { 19 return !input; 20 } 21 // input is undefined here 22 return null; 23}

instanceof Guard#

1class Dog { 2 bark() { 3 return 'Woof!'; 4 } 5} 6 7class Cat { 8 meow() { 9 return 'Meow!'; 10 } 11} 12 13function makeSound(animal: Dog | Cat) { 14 if (animal instanceof Dog) { 15 return animal.bark(); 16 } 17 return animal.meow(); 18} 19 20// Works with built-in types 21function processDate(input: Date | string) { 22 if (input instanceof Date) { 23 return input.toISOString(); 24 } 25 return new Date(input).toISOString(); 26} 27 28// Error handling 29function handleError(error: unknown) { 30 if (error instanceof Error) { 31 return error.message; 32 } 33 if (typeof error === 'string') { 34 return error; 35 } 36 return 'Unknown error'; 37}

in Operator#

1interface Bird { 2 fly(): void; 3 layEggs(): void; 4} 5 6interface Fish { 7 swim(): void; 8 layEggs(): void; 9} 10 11function move(animal: Bird | Fish) { 12 if ('fly' in animal) { 13 animal.fly(); 14 } else { 15 animal.swim(); 16 } 17} 18 19// More complex example 20interface Admin { 21 name: string; 22 privileges: string[]; 23} 24 25interface Employee { 26 name: string; 27 startDate: Date; 28} 29 30type UnknownEmployee = Admin | Employee; 31 32function printEmployee(emp: UnknownEmployee) { 33 console.log(emp.name); 34 35 if ('privileges' in emp) { 36 console.log('Privileges:', emp.privileges); 37 } 38 39 if ('startDate' in emp) { 40 console.log('Started:', emp.startDate); 41 } 42}

Discriminated Unions#

1// Use a literal type as discriminator 2interface Circle { 3 kind: 'circle'; 4 radius: number; 5} 6 7interface Square { 8 kind: 'square'; 9 sideLength: number; 10} 11 12interface Rectangle { 13 kind: 'rectangle'; 14 width: number; 15 height: number; 16} 17 18type Shape = Circle | Square | Rectangle; 19 20function getArea(shape: Shape): number { 21 switch (shape.kind) { 22 case 'circle': 23 return Math.PI * shape.radius ** 2; 24 case 'square': 25 return shape.sideLength ** 2; 26 case 'rectangle': 27 return shape.width * shape.height; 28 } 29} 30 31// Exhaustiveness checking 32function assertNever(x: never): never { 33 throw new Error(`Unexpected value: ${x}`); 34} 35 36function getAreaExhaustive(shape: Shape): number { 37 switch (shape.kind) { 38 case 'circle': 39 return Math.PI * shape.radius ** 2; 40 case 'square': 41 return shape.sideLength ** 2; 42 case 'rectangle': 43 return shape.width * shape.height; 44 default: 45 return assertNever(shape); // Error if case missing 46 } 47}

Custom Type Guards#

1// Type predicate: parameterName is Type 2interface User { 3 name: string; 4 email: string; 5} 6 7interface Admin { 8 name: string; 9 email: string; 10 adminLevel: number; 11} 12 13function isAdmin(user: User | Admin): user is Admin { 14 return 'adminLevel' in user; 15} 16 17function processUser(user: User | Admin) { 18 if (isAdmin(user)) { 19 console.log(`Admin level: ${user.adminLevel}`); 20 } else { 21 console.log(`Regular user: ${user.name}`); 22 } 23} 24 25// Array type guard 26function isStringArray(value: unknown): value is string[] { 27 return ( 28 Array.isArray(value) && 29 value.every(item => typeof item === 'string') 30 ); 31} 32 33// Null check 34function isNotNull<T>(value: T | null): value is T { 35 return value !== null; 36} 37 38const values = [1, null, 2, null, 3]; 39const nonNull = values.filter(isNotNull); 40// Type: number[] 41 42// Defined check 43function isDefined<T>(value: T | undefined): value is T { 44 return value !== undefined; 45}

Assertion Functions#

1// assert function that throws 2function assertIsString(value: unknown): asserts value is string { 3 if (typeof value !== 'string') { 4 throw new Error(`Expected string, got ${typeof value}`); 5 } 6} 7 8function processInput(input: unknown) { 9 assertIsString(input); 10 // TypeScript knows input is string after this point 11 return input.toUpperCase(); 12} 13 14// Assert non-null 15function assertDefined<T>( 16 value: T | undefined | null, 17 message?: string 18): asserts value is T { 19 if (value === undefined || value === null) { 20 throw new Error(message ?? 'Value is undefined or null'); 21 } 22} 23 24function process(maybeValue: string | undefined) { 25 assertDefined(maybeValue, 'Value required'); 26 // maybeValue is string here 27 return maybeValue.length; 28} 29 30// Combine with condition 31function assert( 32 condition: boolean, 33 message: string 34): asserts condition { 35 if (!condition) { 36 throw new Error(message); 37 } 38} 39 40function divide(a: number, b: number): number { 41 assert(b !== 0, 'Division by zero'); 42 return a / b; 43}

Generic Type Guards#

1// Generic type predicate 2function isType<T>( 3 value: unknown, 4 check: (value: unknown) => boolean 5): value is T { 6 return check(value); 7} 8 9interface User { 10 id: number; 11 name: string; 12} 13 14const isUser = (value: unknown): value is User => { 15 return ( 16 typeof value === 'object' && 17 value !== null && 18 'id' in value && 19 'name' in value && 20 typeof (value as User).id === 'number' && 21 typeof (value as User).name === 'string' 22 ); 23}; 24 25// Property guard 26function hasProperty<K extends string>( 27 obj: unknown, 28 prop: K 29): obj is Record<K, unknown> { 30 return typeof obj === 'object' && obj !== null && prop in obj; 31} 32 33function getProperty(obj: unknown, key: string): unknown { 34 if (hasProperty(obj, key)) { 35 return obj[key]; 36 } 37 return undefined; 38}

Narrowing with Control Flow#

1// Assignment narrowing 2let value: string | number; 3value = 'hello'; 4// value is string here 5 6value = 42; 7// value is number here 8 9// Truthiness narrowing 10function printAll(strs: string | string[] | null) { 11 if (strs) { 12 if (typeof strs === 'object') { 13 for (const s of strs) { 14 console.log(s); 15 } 16 } else { 17 console.log(strs); 18 } 19 } 20} 21 22// Equality narrowing 23function compare(x: string | number, y: string | boolean) { 24 if (x === y) { 25 // x and y are both string 26 return x.toUpperCase(); 27 } 28} 29 30// Optional chaining with narrowing 31interface Config { 32 db?: { 33 host: string; 34 port: number; 35 }; 36} 37 38function getHost(config: Config): string { 39 if (config.db?.host) { 40 return config.db.host; // Narrowed 41 } 42 return 'localhost'; 43}

Zod Runtime Validation#

1import { z } from 'zod'; 2 3const UserSchema = z.object({ 4 id: z.number(), 5 name: z.string(), 6 email: z.string().email(), 7}); 8 9type User = z.infer<typeof UserSchema>; 10 11function processUserData(data: unknown): User { 12 // Validates and narrows type 13 return UserSchema.parse(data); 14} 15 16// Safe parse with type guard 17function isValidUser(data: unknown): data is User { 18 return UserSchema.safeParse(data).success; 19} 20 21function handleData(data: unknown) { 22 if (isValidUser(data)) { 23 // data is User here 24 console.log(data.email); 25 } 26}

Best Practices#

Guards: ✓ Use discriminated unions when possible ✓ Keep type guards focused ✓ Use assertion functions for validation ✓ Combine with Zod for runtime safety Patterns: ✓ Prefer 'in' over type assertion ✓ Use exhaustiveness checking ✓ Create reusable type guards ✓ Document complex guards Avoid: ✗ Type assertions without validation ✗ Overly complex type predicates ✗ Ignoring null/undefined ✗ Trusting external data

Conclusion#

Type guards enable safe type narrowing at runtime. Use typeof for primitives, instanceof for classes, and discriminated unions for complex types. Custom type guards with type predicates provide flexibility, while assertion functions enforce invariants. Combine with Zod for robust runtime validation.

Share this article

Help spread the word about Bootspring