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.