Back to Blog
TypeScriptTypesGuardsNarrowing

TypeScript Type Predicates

Master TypeScript type predicates for type narrowing. From user-defined guards to assertion functions.

B
Bootspring Team
Engineering
August 5, 2020
7 min read

Type predicates enable custom type narrowing with user-defined type guards. Here's how to use them effectively.

Basic Type Predicates#

1// Type predicate syntax: paramName is Type 2function isString(value: unknown): value is string { 3 return typeof value === 'string'; 4} 5 6// Usage 7function process(value: unknown) { 8 if (isString(value)) { 9 // TypeScript knows value is string here 10 console.log(value.toUpperCase()); 11 } 12} 13 14// Without type predicate 15function isStringBad(value: unknown): boolean { 16 return typeof value === 'string'; 17} 18 19function processBad(value: unknown) { 20 if (isStringBad(value)) { 21 // TypeScript still sees value as unknown 22 // value.toUpperCase(); // Error! 23 } 24}

Common Type Guards#

1// Array type guard 2function isArray<T>(value: unknown): value is T[] { 3 return Array.isArray(value); 4} 5 6// Object type guard 7function isObject(value: unknown): value is Record<string, unknown> { 8 return typeof value === 'object' && value !== null && !Array.isArray(value); 9} 10 11// Non-null guard 12function isNotNull<T>(value: T | null | undefined): value is T { 13 return value !== null && value !== undefined; 14} 15 16// Filter nulls from array 17const items: (string | null)[] = ['a', null, 'b', null, 'c']; 18const filtered = items.filter(isNotNull); 19// filtered is string[] 20 21// Date guard 22function isDate(value: unknown): value is Date { 23 return value instanceof Date && !isNaN(value.getTime()); 24} 25 26// Function guard 27function isFunction(value: unknown): value is Function { 28 return typeof value === 'function'; 29}

Interface Type Guards#

1interface User { 2 id: number; 3 name: string; 4 email: string; 5} 6 7interface Admin extends User { 8 permissions: string[]; 9} 10 11// Check for specific interface 12function isUser(value: unknown): value is User { 13 return ( 14 typeof value === 'object' && 15 value !== null && 16 'id' in value && 17 'name' in value && 18 'email' in value && 19 typeof (value as User).id === 'number' && 20 typeof (value as User).name === 'string' && 21 typeof (value as User).email === 'string' 22 ); 23} 24 25function isAdmin(value: unknown): value is Admin { 26 return ( 27 isUser(value) && 28 'permissions' in value && 29 Array.isArray((value as Admin).permissions) 30 ); 31} 32 33// Usage 34function handleUser(data: unknown) { 35 if (isAdmin(data)) { 36 console.log(data.permissions); // Admin 37 } else if (isUser(data)) { 38 console.log(data.name); // User 39 } 40}

Discriminated Union Guards#

1// Discriminated unions 2type Shape = 3 | { kind: 'circle'; radius: number } 4 | { kind: 'rectangle'; width: number; height: number } 5 | { kind: 'triangle'; base: number; height: number }; 6 7function isCircle(shape: Shape): shape is { kind: 'circle'; radius: number } { 8 return shape.kind === 'circle'; 9} 10 11function isRectangle(shape: Shape): shape is { kind: 'rectangle'; width: number; height: number } { 12 return shape.kind === 'rectangle'; 13} 14 15// Calculate area 16function calculateArea(shape: Shape): number { 17 if (isCircle(shape)) { 18 return Math.PI * shape.radius ** 2; 19 } 20 if (isRectangle(shape)) { 21 return shape.width * shape.height; 22 } 23 return (shape.base * shape.height) / 2; 24} 25 26// Generic discriminated union guard 27function hasKind<T extends { kind: string }, K extends T['kind']>( 28 value: T, 29 kind: K 30): value is Extract<T, { kind: K }> { 31 return value.kind === kind; 32} 33 34function processShape(shape: Shape) { 35 if (hasKind(shape, 'circle')) { 36 console.log(shape.radius); // Correctly narrowed 37 } 38}

Array Type Guards#

1// Check all elements 2function isStringArray(value: unknown): value is string[] { 3 return Array.isArray(value) && value.every(item => typeof item === 'string'); 4} 5 6// Generic array element check 7function isArrayOf<T>( 8 value: unknown, 9 guard: (item: unknown) => item is T 10): value is T[] { 11 return Array.isArray(value) && value.every(guard); 12} 13 14// Usage 15const data: unknown = ['a', 'b', 'c']; 16 17if (isArrayOf(data, isString)) { 18 // data is string[] 19 data.forEach(s => console.log(s.toUpperCase())); 20} 21 22// Non-empty array guard 23function isNonEmptyArray<T>(value: T[]): value is [T, ...T[]] { 24 return value.length > 0; 25} 26 27function getFirst<T>(arr: T[]): T | undefined { 28 if (isNonEmptyArray(arr)) { 29 return arr[0]; // T, not T | undefined 30 } 31 return undefined; 32}

Assertion Functions#

1// Assertion function (throws if false) 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 8// Usage 9function process(value: unknown) { 10 assertIsString(value); 11 // TypeScript knows value is string after assertion 12 console.log(value.toUpperCase()); 13} 14 15// Assert not null 16function assertDefined<T>(value: T | null | undefined): asserts value is T { 17 if (value === null || value === undefined) { 18 throw new Error('Value is null or undefined'); 19 } 20} 21 22// Assert condition 23function assert(condition: boolean, message?: string): asserts condition { 24 if (!condition) { 25 throw new Error(message || 'Assertion failed'); 26 } 27} 28 29// Usage 30function divide(a: number, b: number): number { 31 assert(b !== 0, 'Cannot divide by zero'); 32 return a / b; // TypeScript knows b !== 0 33}

Generic Type Guards#

1// Generic property check 2function hasProperty<T extends object, K extends string>( 3 obj: T, 4 key: K 5): obj is T & Record<K, unknown> { 6 return key in obj; 7} 8 9// Usage 10function processObject(obj: object) { 11 if (hasProperty(obj, 'name')) { 12 console.log(obj.name); // unknown 13 } 14} 15 16// Property with specific type 17function hasTypedProperty<T extends object, K extends string, V>( 18 obj: T, 19 key: K, 20 guard: (value: unknown) => value is V 21): obj is T & Record<K, V> { 22 return key in obj && guard((obj as any)[key]); 23} 24 25// Usage 26function processData(data: object) { 27 if (hasTypedProperty(data, 'count', isNumber)) { 28 console.log(data.count * 2); // number 29 } 30} 31 32function isNumber(value: unknown): value is number { 33 return typeof value === 'number'; 34}

Complex Type Guards#

1// API response validation 2interface ApiResponse<T> { 3 success: boolean; 4 data: T; 5 error?: string; 6} 7 8function isSuccessResponse<T>( 9 response: ApiResponse<T> 10): response is ApiResponse<T> & { success: true; data: T } { 11 return response.success === true; 12} 13 14function isErrorResponse<T>( 15 response: ApiResponse<T> 16): response is ApiResponse<T> & { success: false; error: string } { 17 return response.success === false && typeof response.error === 'string'; 18} 19 20// Usage 21async function fetchData<T>(): Promise<T> { 22 const response: ApiResponse<T> = await api.get('/data'); 23 24 if (isSuccessResponse(response)) { 25 return response.data; 26 } 27 28 if (isErrorResponse(response)) { 29 throw new Error(response.error); 30 } 31 32 throw new Error('Unknown response format'); 33} 34 35// Schema validation 36interface Schema { 37 [key: string]: 'string' | 'number' | 'boolean' | Schema; 38} 39 40function validateSchema<T>( 41 value: unknown, 42 schema: Schema 43): value is T { 44 if (typeof value !== 'object' || value === null) { 45 return false; 46 } 47 48 for (const [key, type] of Object.entries(schema)) { 49 const propValue = (value as any)[key]; 50 51 if (typeof type === 'object') { 52 if (!validateSchema(propValue, type)) { 53 return false; 54 } 55 } else if (typeof propValue !== type) { 56 return false; 57 } 58 } 59 60 return true; 61} 62 63// Usage 64const userSchema: Schema = { 65 name: 'string', 66 age: 'number', 67 address: { 68 city: 'string', 69 zip: 'string', 70 }, 71}; 72 73function processUser(data: unknown) { 74 if (validateSchema<User>(data, userSchema)) { 75 console.log(data.name); // Typed as User 76 } 77}

Practical Patterns#

1// Form validation 2interface FormData { 3 email: string; 4 password: string; 5} 6 7interface ValidFormData extends FormData { 8 isValid: true; 9} 10 11function isValidForm(data: FormData): data is ValidFormData { 12 return data.email.includes('@') && data.password.length >= 8; 13} 14 15// Event handling 16type MouseEvent = { type: 'click'; x: number; y: number }; 17type KeyboardEvent = { type: 'keypress'; key: string }; 18type Event = MouseEvent | KeyboardEvent; 19 20function isMouseEvent(event: Event): event is MouseEvent { 21 return event.type === 'click'; 22} 23 24function handleEvent(event: Event) { 25 if (isMouseEvent(event)) { 26 console.log(`Clicked at ${event.x}, ${event.y}`); 27 } else { 28 console.log(`Key pressed: ${event.key}`); 29 } 30} 31 32// Error handling 33interface AppError { 34 code: string; 35 message: string; 36} 37 38function isAppError(error: unknown): error is AppError { 39 return ( 40 typeof error === 'object' && 41 error !== null && 42 'code' in error && 43 'message' in error 44 ); 45} 46 47try { 48 // ... 49} catch (error) { 50 if (isAppError(error)) { 51 console.log(`Error ${error.code}: ${error.message}`); 52 } else { 53 console.log('Unknown error'); 54 } 55}

Best Practices#

Design: ✓ Use type predicates for reusable guards ✓ Combine with generics for flexibility ✓ Validate all required properties ✓ Handle edge cases (null, undefined) Safety: ✓ Check types thoroughly ✓ Use assertion functions for invariants ✓ Validate at system boundaries ✓ Test guards with edge cases Performance: ✓ Order checks by likelihood ✓ Fail fast on obvious mismatches ✓ Cache validation results if repeated ✓ Avoid complex recursive checks Avoid: ✗ Type casting without validation ✗ Incomplete property checks ✗ Silent failures in assertions ✗ Over-complicated guards

Conclusion#

Type predicates enable powerful custom type narrowing in TypeScript. Use them to create reusable type guards, validate API responses, and handle discriminated unions. Combine with assertion functions for invariant checks and generics for flexibility.

Share this article

Help spread the word about Bootspring