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.