Back to Blog
TypeScriptType GuardsType NarrowingType Safety

TypeScript Type Guards Guide

Master TypeScript type guards for runtime type checking and type narrowing in your applications.

B
Bootspring Team
Engineering
February 28, 2019
7 min read

Type guards narrow types at runtime, enabling TypeScript to understand what type a value is within a code block. Here's how to use them.

typeof Guards#

1function process(value: string | number) { 2 if (typeof value === 'string') { 3 // TypeScript knows value is string here 4 return value.toUpperCase(); 5 } 6 // TypeScript knows value is number here 7 return value.toFixed(2); 8} 9 10// Works with these types 11function handleValue(value: unknown) { 12 if (typeof value === 'string') return value.trim(); 13 if (typeof value === 'number') return value * 2; 14 if (typeof value === 'boolean') return !value; 15 if (typeof value === 'bigint') return value.toString(); 16 if (typeof value === 'symbol') return value.description; 17 if (typeof value === 'function') return value(); 18 if (typeof value === 'object') return JSON.stringify(value); 19 return value; // undefined 20}

instanceof Guards#

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// With Error types 21function handleError(error: unknown) { 22 if (error instanceof TypeError) { 23 console.log('Type error:', error.message); 24 } else if (error instanceof RangeError) { 25 console.log('Range error:', error.message); 26 } else if (error instanceof Error) { 27 console.log('Error:', error.message); 28 } else { 29 console.log('Unknown error:', error); 30 } 31}

in Operator Guards#

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// With optional properties 20interface Admin { 21 name: string; 22 privileges: string[]; 23} 24 25interface Employee { 26 name: string; 27 startDate: Date; 28} 29 30function printInfo(person: Admin | Employee) { 31 console.log('Name:', person.name); 32 33 if ('privileges' in person) { 34 console.log('Privileges:', person.privileges); 35 } 36 37 if ('startDate' in person) { 38 console.log('Start Date:', person.startDate); 39 } 40}

Custom Type Guards#

1// Type predicate: paramName is Type 2function isString(value: unknown): value is string { 3 return typeof value === 'string'; 4} 5 6function isNumber(value: unknown): value is number { 7 return typeof value === 'number'; 8} 9 10function process(value: unknown) { 11 if (isString(value)) { 12 return value.toUpperCase(); // value is string 13 } 14 if (isNumber(value)) { 15 return value.toFixed(2); // value is number 16 } 17 return null; 18} 19 20// Object type guard 21interface User { 22 id: number; 23 name: string; 24 email: string; 25} 26 27function isUser(obj: unknown): obj is User { 28 return ( 29 typeof obj === 'object' && 30 obj !== null && 31 'id' in obj && 32 'name' in obj && 33 'email' in obj && 34 typeof (obj as User).id === 'number' && 35 typeof (obj as User).name === 'string' && 36 typeof (obj as User).email === 'string' 37 ); 38} 39 40function handleData(data: unknown) { 41 if (isUser(data)) { 42 console.log(data.name); // data is User 43 } 44}

Array Type Guards#

1// Check if array 2function isArray<T>(value: unknown): value is T[] { 3 return Array.isArray(value); 4} 5 6// Check array element types 7function isStringArray(value: unknown): value is string[] { 8 return Array.isArray(value) && value.every((item) => typeof item === 'string'); 9} 10 11function isNumberArray(value: unknown): value is number[] { 12 return Array.isArray(value) && value.every((item) => typeof item === 'number'); 13} 14 15// Non-empty array 16function isNonEmpty<T>(arr: T[]): arr is [T, ...T[]] { 17 return arr.length > 0; 18} 19 20function processArray(arr: number[]) { 21 if (isNonEmpty(arr)) { 22 // arr is [number, ...number[]] 23 const first = arr[0]; // number, not number | undefined 24 } 25}

Discriminated Unions#

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

Nullish Guards#

1// Null/undefined checks 2function process(value: string | null | undefined) { 3 if (value === null) { 4 return 'null'; 5 } 6 7 if (value === undefined) { 8 return 'undefined'; 9 } 10 11 return value.toUpperCase(); // string 12} 13 14// Nullish coalescing alternative 15function processWithDefault(value: string | null | undefined) { 16 const safeValue = value ?? 'default'; 17 return safeValue.toUpperCase(); 18} 19 20// Non-null assertion (use carefully!) 21function nonNullProcess(value: string | null) { 22 // Only when you're certain 23 return value!.toUpperCase(); 24}

Assertion Functions#

1// Assert function throws if condition fails 2function assertIsString(value: unknown): asserts value is string { 3 if (typeof value !== 'string') { 4 throw new Error('Value is not a string'); 5 } 6} 7 8function assertIsNumber(value: unknown): asserts value is number { 9 if (typeof value !== 'number') { 10 throw new Error('Value is not a number'); 11 } 12} 13 14function assertIsDefined<T>(value: T): asserts value is NonNullable<T> { 15 if (value === undefined || value === null) { 16 throw new Error('Value is null or undefined'); 17 } 18} 19 20// Usage 21function process(value: unknown) { 22 assertIsString(value); 23 // After assertion, value is string 24 console.log(value.toUpperCase()); 25} 26 27function processUser(user: User | null) { 28 assertIsDefined(user); 29 // After assertion, user is User 30 console.log(user.name); 31}

Generic Type Guards#

1// Generic type guard factory 2function createTypeGuard<T>( 3 check: (value: unknown) => boolean 4): (value: unknown) => value is T { 5 return (value: unknown): value is T => check(value); 6} 7 8const isPositiveNumber = createTypeGuard<number>( 9 (v) => typeof v === 'number' && v > 0 10); 11 12// Generic property checker 13function hasProperty<T, K extends string>( 14 obj: T, 15 key: K 16): obj is T & Record<K, unknown> { 17 return typeof obj === 'object' && obj !== null && key in obj; 18} 19 20function process(data: unknown) { 21 if (hasProperty(data, 'name')) { 22 console.log(data.name); // unknown, but exists 23 } 24}

API Response Guards#

1interface SuccessResponse<T> { 2 success: true; 3 data: T; 4} 5 6interface ErrorResponse { 7 success: false; 8 error: string; 9} 10 11type ApiResponse<T> = SuccessResponse<T> | ErrorResponse; 12 13function isSuccess<T>( 14 response: ApiResponse<T> 15): response is SuccessResponse<T> { 16 return response.success === true; 17} 18 19function isError<T>( 20 response: ApiResponse<T> 21): response is ErrorResponse { 22 return response.success === false; 23} 24 25async function fetchUser(id: number) { 26 const response: ApiResponse<User> = await api.get(`/users/${id}`); 27 28 if (isSuccess(response)) { 29 return response.data; // User 30 } 31 32 throw new Error(response.error); 33}

Combining Guards#

1// Combine multiple guards 2function isValidUser(value: unknown): value is User { 3 return ( 4 isObject(value) && 5 hasProperty(value, 'id') && 6 hasProperty(value, 'name') && 7 typeof value.id === 'number' && 8 typeof value.name === 'string' 9 ); 10} 11 12function isObject(value: unknown): value is object { 13 return typeof value === 'object' && value !== null; 14} 15 16// Filter with type guards 17const mixed: (string | number)[] = [1, 'a', 2, 'b', 3]; 18 19const strings = mixed.filter((x): x is string => typeof x === 'string'); 20// string[] 21 22const numbers = mixed.filter((x): x is number => typeof x === 'number'); 23// number[]

Best Practices#

Built-in Guards: ✓ typeof for primitives ✓ instanceof for classes ✓ in for property checks ✓ Array.isArray for arrays Custom Guards: ✓ Use type predicates (is) ✓ Validate thoroughly ✓ Handle edge cases ✓ Document expectations Assertion Functions: ✓ Use for fail-fast validation ✓ Throw descriptive errors ✓ Use at boundaries Avoid: ✗ Trusting external data ✗ Incomplete validation ✗ Overusing type assertions ✗ Ignoring null/undefined

Conclusion#

Type guards narrow types at runtime, enabling TypeScript to understand what type a value is within a code block. Use built-in guards (typeof, instanceof, in) for common cases, custom type predicates for complex types, and assertion functions for fail-fast validation. Discriminated unions with literal type discriminants provide exhaustive type checking. Always validate external data thoroughly and prefer type guards over type assertions for type safety.

Share this article

Help spread the word about Bootspring