Type guards narrow types at runtime. Here's how to use them effectively.
typeof Guard#
1function processValue(value: string | number) {
2 if (typeof value === 'string') {
3 // TypeScript knows value is string
4 return value.toUpperCase();
5 }
6 // TypeScript knows value is number
7 return value.toFixed(2);
8}
9
10// typeof works for primitives
11function handle(input: string | number | boolean | undefined) {
12 if (typeof input === 'string') {
13 return input.trim();
14 }
15 if (typeof input === 'number') {
16 return input * 2;
17 }
18 if (typeof input === 'boolean') {
19 return !input;
20 }
21 // input is undefined here
22 return null;
23}instanceof Guard#
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// Works with built-in types
21function processDate(input: Date | string) {
22 if (input instanceof Date) {
23 return input.toISOString();
24 }
25 return new Date(input).toISOString();
26}
27
28// Error handling
29function handleError(error: unknown) {
30 if (error instanceof Error) {
31 return error.message;
32 }
33 if (typeof error === 'string') {
34 return error;
35 }
36 return 'Unknown error';
37}in Operator#
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// More complex example
20interface Admin {
21 name: string;
22 privileges: string[];
23}
24
25interface Employee {
26 name: string;
27 startDate: Date;
28}
29
30type UnknownEmployee = Admin | Employee;
31
32function printEmployee(emp: UnknownEmployee) {
33 console.log(emp.name);
34
35 if ('privileges' in emp) {
36 console.log('Privileges:', emp.privileges);
37 }
38
39 if ('startDate' in emp) {
40 console.log('Started:', emp.startDate);
41 }
42}Discriminated Unions#
1// Use a literal type as discriminator
2interface Circle {
3 kind: 'circle';
4 radius: number;
5}
6
7interface Square {
8 kind: 'square';
9 sideLength: number;
10}
11
12interface Rectangle {
13 kind: 'rectangle';
14 width: number;
15 height: number;
16}
17
18type Shape = Circle | Square | Rectangle;
19
20function getArea(shape: Shape): number {
21 switch (shape.kind) {
22 case 'circle':
23 return Math.PI * shape.radius ** 2;
24 case 'square':
25 return shape.sideLength ** 2;
26 case 'rectangle':
27 return shape.width * shape.height;
28 }
29}
30
31// Exhaustiveness checking
32function assertNever(x: never): never {
33 throw new Error(`Unexpected value: ${x}`);
34}
35
36function getAreaExhaustive(shape: Shape): number {
37 switch (shape.kind) {
38 case 'circle':
39 return Math.PI * shape.radius ** 2;
40 case 'square':
41 return shape.sideLength ** 2;
42 case 'rectangle':
43 return shape.width * shape.height;
44 default:
45 return assertNever(shape); // Error if case missing
46 }
47}Custom Type Guards#
1// Type predicate: parameterName is Type
2interface User {
3 name: string;
4 email: string;
5}
6
7interface Admin {
8 name: string;
9 email: string;
10 adminLevel: number;
11}
12
13function isAdmin(user: User | Admin): user is Admin {
14 return 'adminLevel' in user;
15}
16
17function processUser(user: User | Admin) {
18 if (isAdmin(user)) {
19 console.log(`Admin level: ${user.adminLevel}`);
20 } else {
21 console.log(`Regular user: ${user.name}`);
22 }
23}
24
25// Array type guard
26function isStringArray(value: unknown): value is string[] {
27 return (
28 Array.isArray(value) &&
29 value.every(item => typeof item === 'string')
30 );
31}
32
33// Null check
34function isNotNull<T>(value: T | null): value is T {
35 return value !== null;
36}
37
38const values = [1, null, 2, null, 3];
39const nonNull = values.filter(isNotNull);
40// Type: number[]
41
42// Defined check
43function isDefined<T>(value: T | undefined): value is T {
44 return value !== undefined;
45}Assertion Functions#
1// assert function that throws
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
8function processInput(input: unknown) {
9 assertIsString(input);
10 // TypeScript knows input is string after this point
11 return input.toUpperCase();
12}
13
14// Assert non-null
15function assertDefined<T>(
16 value: T | undefined | null,
17 message?: string
18): asserts value is T {
19 if (value === undefined || value === null) {
20 throw new Error(message ?? 'Value is undefined or null');
21 }
22}
23
24function process(maybeValue: string | undefined) {
25 assertDefined(maybeValue, 'Value required');
26 // maybeValue is string here
27 return maybeValue.length;
28}
29
30// Combine with condition
31function assert(
32 condition: boolean,
33 message: string
34): asserts condition {
35 if (!condition) {
36 throw new Error(message);
37 }
38}
39
40function divide(a: number, b: number): number {
41 assert(b !== 0, 'Division by zero');
42 return a / b;
43}Generic Type Guards#
1// Generic type predicate
2function isType<T>(
3 value: unknown,
4 check: (value: unknown) => boolean
5): value is T {
6 return check(value);
7}
8
9interface User {
10 id: number;
11 name: string;
12}
13
14const isUser = (value: unknown): value is User => {
15 return (
16 typeof value === 'object' &&
17 value !== null &&
18 'id' in value &&
19 'name' in value &&
20 typeof (value as User).id === 'number' &&
21 typeof (value as User).name === 'string'
22 );
23};
24
25// Property guard
26function hasProperty<K extends string>(
27 obj: unknown,
28 prop: K
29): obj is Record<K, unknown> {
30 return typeof obj === 'object' && obj !== null && prop in obj;
31}
32
33function getProperty(obj: unknown, key: string): unknown {
34 if (hasProperty(obj, key)) {
35 return obj[key];
36 }
37 return undefined;
38}Narrowing with Control Flow#
1// Assignment narrowing
2let value: string | number;
3value = 'hello';
4// value is string here
5
6value = 42;
7// value is number here
8
9// Truthiness narrowing
10function printAll(strs: string | string[] | null) {
11 if (strs) {
12 if (typeof strs === 'object') {
13 for (const s of strs) {
14 console.log(s);
15 }
16 } else {
17 console.log(strs);
18 }
19 }
20}
21
22// Equality narrowing
23function compare(x: string | number, y: string | boolean) {
24 if (x === y) {
25 // x and y are both string
26 return x.toUpperCase();
27 }
28}
29
30// Optional chaining with narrowing
31interface Config {
32 db?: {
33 host: string;
34 port: number;
35 };
36}
37
38function getHost(config: Config): string {
39 if (config.db?.host) {
40 return config.db.host; // Narrowed
41 }
42 return 'localhost';
43}Zod Runtime Validation#
1import { z } from 'zod';
2
3const UserSchema = z.object({
4 id: z.number(),
5 name: z.string(),
6 email: z.string().email(),
7});
8
9type User = z.infer<typeof UserSchema>;
10
11function processUserData(data: unknown): User {
12 // Validates and narrows type
13 return UserSchema.parse(data);
14}
15
16// Safe parse with type guard
17function isValidUser(data: unknown): data is User {
18 return UserSchema.safeParse(data).success;
19}
20
21function handleData(data: unknown) {
22 if (isValidUser(data)) {
23 // data is User here
24 console.log(data.email);
25 }
26}Best Practices#
Guards:
✓ Use discriminated unions when possible
✓ Keep type guards focused
✓ Use assertion functions for validation
✓ Combine with Zod for runtime safety
Patterns:
✓ Prefer 'in' over type assertion
✓ Use exhaustiveness checking
✓ Create reusable type guards
✓ Document complex guards
Avoid:
✗ Type assertions without validation
✗ Overly complex type predicates
✗ Ignoring null/undefined
✗ Trusting external data
Conclusion#
Type guards enable safe type narrowing at runtime. Use typeof for primitives, instanceof for classes, and discriminated unions for complex types. Custom type guards with type predicates provide flexibility, while assertion functions enforce invariants. Combine with Zod for robust runtime validation.