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.