Assertion functions narrow types by throwing on invalid conditions. Here's how to use them.
Basic Assertion Functions#
1// Assertion function syntax
2function assert(condition: boolean, message?: string): asserts condition {
3 if (!condition) {
4 throw new Error(message || 'Assertion failed');
5 }
6}
7
8// Usage - TypeScript narrows type after assertion
9function process(value: string | null) {
10 assert(value !== null, 'Value cannot be null');
11 // TypeScript knows value is string here
12 console.log(value.toUpperCase());
13}
14
15// Type predicate assertion
16function assertIsString(value: unknown): asserts value is string {
17 if (typeof value !== 'string') {
18 throw new TypeError(`Expected string, got ${typeof value}`);
19 }
20}
21
22// Usage
23function handleInput(input: unknown) {
24 assertIsString(input);
25 // TypeScript knows input is string
26 console.log(input.length);
27}Common Assertion Functions#
1// Assert defined (not null/undefined)
2function assertDefined<T>(
3 value: T | null | undefined,
4 message?: string
5): asserts value is T {
6 if (value === null || value === undefined) {
7 throw new Error(message || 'Value is null or undefined');
8 }
9}
10
11// Assert non-null
12function assertNonNull<T>(
13 value: T | null,
14 message?: string
15): asserts value is T {
16 if (value === null) {
17 throw new Error(message || 'Value is null');
18 }
19}
20
21// Assert array
22function assertIsArray<T>(
23 value: unknown,
24 message?: string
25): asserts value is T[] {
26 if (!Array.isArray(value)) {
27 throw new TypeError(message || 'Value is not an array');
28 }
29}
30
31// Assert number
32function assertIsNumber(
33 value: unknown,
34 message?: string
35): asserts value is number {
36 if (typeof value !== 'number' || isNaN(value)) {
37 throw new TypeError(message || 'Value is not a valid number');
38 }
39}
40
41// Assert object
42function assertIsObject(
43 value: unknown,
44 message?: string
45): asserts value is Record<string, unknown> {
46 if (typeof value !== 'object' || value === null || Array.isArray(value)) {
47 throw new TypeError(message || 'Value is not an object');
48 }
49}Interface Assertions#
1interface User {
2 id: number;
3 name: string;
4 email: string;
5}
6
7function assertIsUser(value: unknown): asserts value is User {
8 if (typeof value !== 'object' || value === null) {
9 throw new TypeError('Expected object');
10 }
11
12 const obj = value as Record<string, unknown>;
13
14 if (typeof obj.id !== 'number') {
15 throw new TypeError('Expected id to be a number');
16 }
17 if (typeof obj.name !== 'string') {
18 throw new TypeError('Expected name to be a string');
19 }
20 if (typeof obj.email !== 'string') {
21 throw new TypeError('Expected email to be a string');
22 }
23}
24
25// Usage
26function processUser(data: unknown) {
27 assertIsUser(data);
28 // TypeScript knows data is User
29 console.log(`User: ${data.name} (${data.email})`);
30}
31
32// With validation details
33interface ValidationError {
34 field: string;
35 message: string;
36}
37
38function assertIsUserDetailed(
39 value: unknown
40): asserts value is User {
41 const errors: ValidationError[] = [];
42
43 if (typeof value !== 'object' || value === null) {
44 throw new Error('Expected object');
45 }
46
47 const obj = value as Record<string, unknown>;
48
49 if (typeof obj.id !== 'number') {
50 errors.push({ field: 'id', message: 'Expected number' });
51 }
52 if (typeof obj.name !== 'string') {
53 errors.push({ field: 'name', message: 'Expected string' });
54 }
55 if (typeof obj.email !== 'string' || !obj.email.includes('@')) {
56 errors.push({ field: 'email', message: 'Expected valid email' });
57 }
58
59 if (errors.length > 0) {
60 const error = new Error('Validation failed');
61 (error as any).errors = errors;
62 throw error;
63 }
64}Discriminated Union Assertions#
1type Shape =
2 | { kind: 'circle'; radius: number }
3 | { kind: 'rectangle'; width: number; height: number }
4 | { kind: 'triangle'; base: number; height: number };
5
6function assertIsCircle(
7 shape: Shape
8): asserts shape is { kind: 'circle'; radius: number } {
9 if (shape.kind !== 'circle') {
10 throw new Error(`Expected circle, got ${shape.kind}`);
11 }
12}
13
14function assertIsRectangle(
15 shape: Shape
16): asserts shape is { kind: 'rectangle'; width: number; height: number } {
17 if (shape.kind !== 'rectangle') {
18 throw new Error(`Expected rectangle, got ${shape.kind}`);
19 }
20}
21
22// Generic assertion for discriminated unions
23function assertKind<T extends { kind: string }, K extends T['kind']>(
24 value: T,
25 kind: K
26): asserts value is Extract<T, { kind: K }> {
27 if (value.kind !== kind) {
28 throw new Error(`Expected ${kind}, got ${value.kind}`);
29 }
30}
31
32// Usage
33function processShape(shape: Shape) {
34 assertKind(shape, 'circle');
35 // TypeScript knows shape is circle
36 console.log(`Area: ${Math.PI * shape.radius ** 2}`);
37}Conditional Assertions#
1// Assert with condition
2function assertCondition<T>(
3 value: T | null | undefined,
4 condition: (value: T) => boolean,
5 message?: string
6): asserts value is T {
7 if (value === null || value === undefined) {
8 throw new Error(message || 'Value is null or undefined');
9 }
10 if (!condition(value)) {
11 throw new Error(message || 'Condition not met');
12 }
13}
14
15// Usage
16function processNumber(value: number | null) {
17 assertCondition(value, (n) => n > 0, 'Value must be positive');
18 // TypeScript knows value is number (positive)
19 console.log(Math.sqrt(value));
20}
21
22// Assert array not empty
23function assertNonEmptyArray<T>(
24 value: T[]
25): asserts value is [T, ...T[]] {
26 if (value.length === 0) {
27 throw new Error('Array is empty');
28 }
29}
30
31// Usage
32function getFirst<T>(arr: T[]): T {
33 assertNonEmptyArray(arr);
34 return arr[0]; // No undefined in return type!
35}Error Classes#
1// Custom assertion errors
2class AssertionError extends Error {
3 constructor(message: string) {
4 super(message);
5 this.name = 'AssertionError';
6 }
7}
8
9class TypeAssertionError extends AssertionError {
10 constructor(
11 public expected: string,
12 public received: string
13 ) {
14 super(`Expected ${expected}, received ${received}`);
15 this.name = 'TypeAssertionError';
16 }
17}
18
19function assertIsString(value: unknown): asserts value is string {
20 if (typeof value !== 'string') {
21 throw new TypeAssertionError('string', typeof value);
22 }
23}
24
25// With stack trace preservation
26function createAssertionError(message: string): Error {
27 const error = new AssertionError(message);
28 Error.captureStackTrace?.(error, createAssertionError);
29 return error;
30}Runtime Invariants#
1// Invariant assertion (never returns normally if condition is false)
2function invariant(
3 condition: boolean,
4 message?: string
5): asserts condition {
6 if (!condition) {
7 throw new Error(`Invariant violation: ${message || 'Unexpected state'}`);
8 }
9}
10
11// Usage in state management
12class Counter {
13 private count: number = 0;
14
15 increment() {
16 this.count++;
17 invariant(this.count > 0, 'Count must be positive after increment');
18 }
19
20 decrement() {
21 invariant(this.count > 0, 'Cannot decrement below zero');
22 this.count--;
23 }
24}
25
26// Usage in unreachable code
27function exhaustiveCheck(value: never): never {
28 throw new Error(`Unhandled case: ${value}`);
29}
30
31type Status = 'pending' | 'active' | 'completed';
32
33function handleStatus(status: Status) {
34 switch (status) {
35 case 'pending':
36 return 'Waiting...';
37 case 'active':
38 return 'In progress';
39 case 'completed':
40 return 'Done';
41 default:
42 return exhaustiveCheck(status);
43 }
44}Combining with Type Guards#
1// Type guard (returns boolean)
2function isString(value: unknown): value is string {
3 return typeof value === 'string';
4}
5
6// Assertion (throws or narrows)
7function assertIsString(value: unknown): asserts value is string {
8 if (!isString(value)) {
9 throw new TypeError('Expected string');
10 }
11}
12
13// Use type guard for conditional logic
14function maybeProcess(value: unknown) {
15 if (isString(value)) {
16 console.log(value.toUpperCase());
17 }
18}
19
20// Use assertion for required values
21function mustProcess(value: unknown) {
22 assertIsString(value);
23 console.log(value.toUpperCase());
24}
25
26// Convert guard to assertion
27function toAssertion<T>(
28 guard: (value: unknown) => value is T,
29 message?: string
30): (value: unknown) => asserts value is T {
31 return (value: unknown): asserts value is T => {
32 if (!guard(value)) {
33 throw new Error(message || 'Assertion failed');
34 }
35 };
36}
37
38const assertString = toAssertion(isString, 'Expected string');API Response Validation#
1interface ApiResponse<T> {
2 success: boolean;
3 data?: T;
4 error?: string;
5}
6
7function assertSuccess<T>(
8 response: ApiResponse<T>
9): asserts response is ApiResponse<T> & { success: true; data: T } {
10 if (!response.success) {
11 throw new Error(response.error || 'Request failed');
12 }
13 if (response.data === undefined) {
14 throw new Error('Response data is missing');
15 }
16}
17
18// Usage
19async function fetchUser(id: string): Promise<User> {
20 const response = await api.get<User>(`/users/${id}`);
21 assertSuccess(response);
22 return response.data;
23}
24
25// Assert response shape
26function assertApiResponse<T>(
27 value: unknown,
28 dataValidator: (data: unknown) => asserts data is T
29): asserts value is ApiResponse<T> & { success: true; data: T } {
30 assertIsObject(value);
31
32 if (typeof value.success !== 'boolean') {
33 throw new Error('Missing success field');
34 }
35
36 if (!value.success) {
37 throw new Error(String(value.error) || 'Request failed');
38 }
39
40 dataValidator(value.data);
41}Best Practices#
Design:
✓ Clear error messages
✓ Use custom error classes
✓ Preserve stack traces
✓ Document assertion behavior
Usage:
✓ Validate at boundaries
✓ Assert invariants
✓ Use for required values
✓ Combine with type guards
Performance:
✓ Assertions can be stripped in production
✓ Keep validation logic simple
✓ Avoid expensive checks in hot paths
✓ Consider lazy validation
Avoid:
✗ Silent failures
✗ Overly complex assertions
✗ Assertions for flow control
✗ Ignoring assertion errors
Conclusion#
Assertion functions provide runtime type checking with TypeScript type narrowing. Use them for validating external data, enforcing invariants, and ensuring type safety at runtime. Combine with custom error classes and clear messages for better debugging.