Back to Blog
TypeScriptType PredicatesType GuardsTypes

TypeScript Type Predicates Guide

Master TypeScript type predicates for custom type guards and runtime type checking.

B
Bootspring Team
Engineering
November 24, 2018
6 min read

Type predicates allow you to create custom type guards that narrow types at runtime. Here's how to use them.

Basic Type Predicate#

1// Type predicate syntax: paramName is Type 2function isString(value: unknown): value is string { 3 return typeof value === 'string'; 4} 5 6// Usage - TypeScript narrows the type 7function processValue(value: unknown) { 8 if (isString(value)) { 9 // value is string here 10 console.log(value.toUpperCase()); 11 } 12} 13 14// Without type predicate, you'd need to cast 15function processValueWithoutPredicate(value: unknown) { 16 if (typeof value === 'string') { 17 console.log(value.toUpperCase()); // Works, but inline check 18 } 19}

Object 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 User 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 25// Check for Admin 26function isAdmin(user: User): user is Admin { 27 return 'permissions' in user && Array.isArray((user as Admin).permissions); 28} 29 30// Usage 31function handleUser(data: unknown) { 32 if (isUser(data)) { 33 console.log(data.name); // data is User 34 35 if (isAdmin(data)) { 36 console.log(data.permissions); // data is Admin 37 } 38 } 39}

Array Type Guards#

1// Check if array of specific type 2function isStringArray(value: unknown): value is string[] { 3 return ( 4 Array.isArray(value) && 5 value.every(item => typeof item === 'string') 6 ); 7} 8 9function isNumberArray(value: unknown): value is number[] { 10 return ( 11 Array.isArray(value) && 12 value.every(item => typeof item === 'number') 13 ); 14} 15 16// Generic array guard 17function isArrayOf<T>( 18 value: unknown, 19 guard: (item: unknown) => item is T 20): value is T[] { 21 return Array.isArray(value) && value.every(guard); 22} 23 24// Usage 25const data: unknown = ['a', 'b', 'c']; 26 27if (isArrayOf(data, isString)) { 28 // data is string[] 29 data.forEach(str => console.log(str.toUpperCase())); 30}

Discriminated Union Guards#

1interface Circle { 2 kind: 'circle'; 3 radius: number; 4} 5 6interface Rectangle { 7 kind: 'rectangle'; 8 width: number; 9 height: number; 10} 11 12type Shape = Circle | Rectangle; 13 14// Type predicate for discriminated union 15function isCircle(shape: Shape): shape is Circle { 16 return shape.kind === 'circle'; 17} 18 19function isRectangle(shape: Shape): shape is Rectangle { 20 return shape.kind === 'rectangle'; 21} 22 23// Usage 24function getArea(shape: Shape): number { 25 if (isCircle(shape)) { 26 return Math.PI * shape.radius ** 2; 27 } 28 return shape.width * shape.height; 29}

Null and Undefined Guards#

1// Non-null assertion 2function isDefined<T>(value: T | null | undefined): value is T { 3 return value !== null && value !== undefined; 4} 5 6// Filter nullish values 7const items: (string | null | undefined)[] = ['a', null, 'b', undefined, 'c']; 8const definedItems: string[] = items.filter(isDefined); 9 10// Non-empty string 11function isNonEmptyString(value: unknown): value is string { 12 return typeof value === 'string' && value.length > 0; 13} 14 15// Usage with optional chaining 16function processUser(user: User | null) { 17 if (isDefined(user)) { 18 console.log(user.name); // user is User 19 } 20}

Class Instance Guards#

1class HttpError extends Error { 2 constructor(public statusCode: number, message: string) { 3 super(message); 4 } 5} 6 7class ValidationError extends Error { 8 constructor(public fields: string[]) { 9 super('Validation failed'); 10 } 11} 12 13// Instance type guards 14function isHttpError(error: unknown): error is HttpError { 15 return error instanceof HttpError; 16} 17 18function isValidationError(error: unknown): error is ValidationError { 19 return error instanceof ValidationError; 20} 21 22// Usage 23function handleError(error: unknown) { 24 if (isHttpError(error)) { 25 console.log(`HTTP ${error.statusCode}: ${error.message}`); 26 } else if (isValidationError(error)) { 27 console.log('Invalid fields:', error.fields.join(', ')); 28 } else if (error instanceof Error) { 29 console.log('Error:', error.message); 30 } 31}

Combining Type Guards#

1// AND combination 2function isAdminUser(value: unknown): value is Admin { 3 return isUser(value) && isAdmin(value); 4} 5 6// OR with union 7type StringOrNumber = string | number; 8 9function isStringOrNumber(value: unknown): value is StringOrNumber { 10 return typeof value === 'string' || typeof value === 'number'; 11} 12 13// Negation 14function isNotNull<T>(value: T | null): value is T { 15 return value !== null; 16} 17 18// Chained guards 19function processData(data: unknown) { 20 if (!isUser(data)) return; 21 // data is User 22 23 if (!isAdmin(data)) { 24 console.log('Regular user:', data.name); 25 return; 26 } 27 28 // data is Admin 29 console.log('Admin with permissions:', data.permissions); 30}

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 13// Generic response guard 14function isSuccess<T>( 15 response: ApiResponse<T> 16): response is SuccessResponse<T> { 17 return response.success === true; 18} 19 20function isError<T>( 21 response: ApiResponse<T> 22): response is ErrorResponse { 23 return response.success === false; 24} 25 26// Usage 27async function fetchUser(id: number) { 28 const response = await api.get<ApiResponse<User>>(`/users/${id}`); 29 30 if (isSuccess(response)) { 31 return response.data; // User 32 } 33 34 throw new Error(response.error); 35}

Form Data Guards#

1interface FormData { 2 email?: string; 3 password?: string; 4 name?: string; 5} 6 7interface ValidFormData { 8 email: string; 9 password: string; 10 name: string; 11} 12 13function isValidFormData(data: FormData): data is ValidFormData { 14 return ( 15 typeof data.email === 'string' && 16 data.email.includes('@') && 17 typeof data.password === 'string' && 18 data.password.length >= 8 && 19 typeof data.name === 'string' && 20 data.name.length > 0 21 ); 22} 23 24// Usage 25function submitForm(data: FormData) { 26 if (!isValidFormData(data)) { 27 throw new Error('Invalid form data'); 28 } 29 30 // data is ValidFormData - all fields guaranteed 31 sendToApi({ 32 email: data.email, 33 password: data.password, 34 name: data.name, 35 }); 36}

JSON Type Guards#

1type Json = 2 | string 3 | number 4 | boolean 5 | null 6 | Json[] 7 | { [key: string]: Json }; 8 9function isJsonObject(value: Json): value is { [key: string]: Json } { 10 return typeof value === 'object' && value !== null && !Array.isArray(value); 11} 12 13function isJsonArray(value: Json): value is Json[] { 14 return Array.isArray(value); 15} 16 17// Safe JSON parsing 18function parseJson(text: string): Json | undefined { 19 try { 20 return JSON.parse(text); 21 } catch { 22 return undefined; 23 } 24} 25 26function getJsonProperty(json: Json, key: string): Json | undefined { 27 if (isJsonObject(json)) { 28 return json[key]; 29 } 30 return undefined; 31}

Best Practices#

Design: ✓ Return boolean, narrow type ✓ Check all required properties ✓ Handle null and undefined ✓ Use descriptive names Implementation: ✓ Validate all conditions ✓ Use in and typeof checks ✓ Consider instanceof for classes ✓ Combine guards for complex types Patterns: ✓ API response validation ✓ Form data validation ✓ Filter array methods ✓ Discriminated unions Avoid: ✗ Type casting inside guards ✗ Incomplete validation ✗ Overly complex predicates ✗ Side effects in guards

Conclusion#

Type predicates create reusable type guards that narrow types at runtime. Use them for API validation, form data checking, discriminated unions, and filtering arrays. Keep predicates focused on type checking without side effects. Combine with generics for flexible, reusable guards across your codebase.

Share this article

Help spread the word about Bootspring