Assertion functions validate conditions at runtime and narrow types, throwing errors when assertions fail.
Basic Assertion Functions#
1// Assertion function signature
2function assert(condition: boolean, message?: string): asserts condition {
3 if (!condition) {
4 throw new Error(message || 'Assertion failed');
5 }
6}
7
8// Usage
9function divide(a: number, b: number): number {
10 assert(b !== 0, 'Cannot divide by zero');
11 return a / b;
12}
13
14// After assertion, condition is known to be true
15function processArray(arr: number[] | null) {
16 assert(arr !== null, 'Array must not be null');
17 // TypeScript knows arr is number[] here
18 return arr.map(x => x * 2);
19}Type Assertion Functions#
1// Assert value is specific type
2function assertIsString(value: unknown): asserts value is string {
3 if (typeof value !== 'string') {
4 throw new TypeError(`Expected string, got ${typeof value}`);
5 }
6}
7
8function processValue(value: unknown) {
9 assertIsString(value);
10 // TypeScript knows value is string here
11 console.log(value.toUpperCase());
12}
13
14// Assert value is not null/undefined
15function assertDefined<T>(
16 value: T | null | undefined,
17 message?: string
18): asserts value is T {
19 if (value === null || value === undefined) {
20 throw new Error(message || 'Value must be defined');
21 }
22}
23
24function getUser(id: string) {
25 const user = users.get(id);
26 assertDefined(user, `User ${id} not found`);
27 // TypeScript knows user is User, not User | undefined
28 return user.name;
29}Object Type Assertions#
1// Assert object has specific shape
2interface User {
3 id: number;
4 name: string;
5 email: string;
6}
7
8function assertIsUser(value: unknown): asserts value is User {
9 if (typeof value !== 'object' || value === null) {
10 throw new TypeError('Expected object');
11 }
12
13 const obj = value as Record<string, unknown>;
14
15 if (typeof obj.id !== 'number') {
16 throw new TypeError('id must be a number');
17 }
18 if (typeof obj.name !== 'string') {
19 throw new TypeError('name must be a string');
20 }
21 if (typeof obj.email !== 'string') {
22 throw new TypeError('email must be a string');
23 }
24}
25
26function processApiResponse(data: unknown) {
27 assertIsUser(data);
28 // TypeScript knows data is User
29 console.log(`Welcome, ${data.name}!`);
30}
31
32// Generic object assertion
33function assertHasProperty<K extends string>(
34 obj: object,
35 key: K
36): asserts obj is object & Record<K, unknown> {
37 if (!(key in obj)) {
38 throw new Error(`Object missing property: ${key}`);
39 }
40}Array Assertions#
1// Assert non-empty array
2function assertNonEmpty<T>(
3 arr: T[],
4 message?: string
5): asserts arr is [T, ...T[]] {
6 if (arr.length === 0) {
7 throw new Error(message || 'Array must not be empty');
8 }
9}
10
11function processItems(items: number[]) {
12 assertNonEmpty(items, 'Need at least one item');
13 // TypeScript knows items has at least one element
14 const [first, ...rest] = items;
15 console.log('First:', first); // first is number, not number | undefined
16}
17
18// Assert array of specific type
19function assertIsStringArray(
20 value: unknown
21): asserts value is string[] {
22 if (!Array.isArray(value)) {
23 throw new TypeError('Expected array');
24 }
25 if (!value.every(item => typeof item === 'string')) {
26 throw new TypeError('All items must be strings');
27 }
28}Combining with Type Guards#
1// Type guard (returns boolean)
2function isUser(value: unknown): value is User {
3 return (
4 typeof value === 'object' &&
5 value !== null &&
6 'id' in value &&
7 'name' in value
8 );
9}
10
11// Assertion function (throws or narrows)
12function assertIsUser(value: unknown): asserts value is User {
13 if (!isUser(value)) {
14 throw new TypeError('Invalid user object');
15 }
16}
17
18// Use guard for conditional logic
19function maybeProcess(data: unknown) {
20 if (isUser(data)) {
21 // Optional path
22 console.log(data.name);
23 }
24}
25
26// Use assertion for required validation
27function mustProcess(data: unknown) {
28 assertIsUser(data);
29 // Required - throws if not user
30 console.log(data.name);
31}Class Instance Assertions#
1// Assert instance of class
2function assertInstanceOf<T>(
3 value: unknown,
4 constructor: new (...args: any[]) => T,
5 message?: string
6): asserts value is T {
7 if (!(value instanceof constructor)) {
8 throw new TypeError(
9 message || `Expected instance of ${constructor.name}`
10 );
11 }
12}
13
14class ApiError extends Error {
15 constructor(public code: number, message: string) {
16 super(message);
17 }
18}
19
20function handleError(error: unknown) {
21 assertInstanceOf(error, ApiError, 'Expected API error');
22 // TypeScript knows error is ApiError
23 console.log(`API Error ${error.code}: ${error.message}`);
24}Validation Chains#
1// Build validation chains
2class Validator<T> {
3 constructor(private value: T) {}
4
5 assert(
6 condition: boolean,
7 message: string
8 ): Validator<T> {
9 if (!condition) {
10 throw new Error(message);
11 }
12 return this;
13 }
14
15 assertString(): Validator<T & string> {
16 if (typeof this.value !== 'string') {
17 throw new TypeError('Expected string');
18 }
19 return this as Validator<T & string>;
20 }
21
22 assertMinLength(min: number): this {
23 if (String(this.value).length < min) {
24 throw new Error(`Minimum length is ${min}`);
25 }
26 return this;
27 }
28
29 get(): T {
30 return this.value;
31 }
32}
33
34function validate<T>(value: T): Validator<T> {
35 return new Validator(value);
36}
37
38// Usage
39function processUsername(input: unknown) {
40 const username = validate(input)
41 .assertString()
42 .assertMinLength(3)
43 .get();
44
45 // username is string
46 return username.toLowerCase();
47}Form Validation#
1interface FormData {
2 email: string;
3 password: string;
4 age: number;
5}
6
7function assertValidEmail(email: unknown): asserts email is string {
8 assertIsString(email);
9 if (!email.includes('@')) {
10 throw new Error('Invalid email format');
11 }
12}
13
14function assertValidPassword(password: unknown): asserts password is string {
15 assertIsString(password);
16 if (password.length < 8) {
17 throw new Error('Password must be at least 8 characters');
18 }
19}
20
21function assertValidAge(age: unknown): asserts age is number {
22 if (typeof age !== 'number' || isNaN(age)) {
23 throw new Error('Age must be a number');
24 }
25 if (age < 0 || age > 150) {
26 throw new Error('Age must be between 0 and 150');
27 }
28}
29
30function validateForm(data: unknown): FormData {
31 if (typeof data !== 'object' || data === null) {
32 throw new Error('Invalid form data');
33 }
34
35 const { email, password, age } = data as Record<string, unknown>;
36
37 assertValidEmail(email);
38 assertValidPassword(password);
39 assertValidAge(age);
40
41 return { email, password, age };
42}API Response Validation#
1interface ApiResponse<T> {
2 success: boolean;
3 data: T;
4 error?: string;
5}
6
7function assertSuccessResponse<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}
14
15async function fetchUser(id: string): Promise<User> {
16 const response: ApiResponse<User> = await fetch(`/api/users/${id}`)
17 .then(r => r.json());
18
19 assertSuccessResponse(response);
20
21 // TypeScript knows response.data is User
22 return response.data;
23}
24
25// Discriminated union version
26type Result<T> =
27 | { ok: true; value: T }
28 | { ok: false; error: string };
29
30function assertOk<T>(result: Result<T>): asserts result is { ok: true; value: T } {
31 if (!result.ok) {
32 throw new Error(result.error);
33 }
34}Environment Assertions#
1// Assert environment variables
2function assertEnv(
3 key: string
4): asserts process.env is Record<string, string> & Record<typeof key, string> {
5 if (!process.env[key]) {
6 throw new Error(`Missing environment variable: ${key}`);
7 }
8}
9
10function getConfig() {
11 assertEnv('DATABASE_URL');
12 assertEnv('API_KEY');
13 assertEnv('SECRET');
14
15 return {
16 databaseUrl: process.env.DATABASE_URL,
17 apiKey: process.env.API_KEY,
18 secret: process.env.SECRET
19 };
20}
21
22// Generic environment helper
23function requireEnv(key: string): string {
24 const value = process.env[key];
25 if (!value) {
26 throw new Error(`Missing environment variable: ${key}`);
27 }
28 return value;
29}Error Handling#
1// Narrow error types
2function assertError(value: unknown): asserts value is Error {
3 if (!(value instanceof Error)) {
4 throw new TypeError('Expected Error instance');
5 }
6}
7
8async function safeOperation() {
9 try {
10 await riskyOperation();
11 } catch (error) {
12 assertError(error);
13 // TypeScript knows error is Error
14 console.error('Operation failed:', error.message);
15 console.error('Stack:', error.stack);
16 }
17}
18
19// Custom error assertion
20interface ApiError {
21 code: string;
22 message: string;
23 details?: Record<string, string>;
24}
25
26function assertApiError(value: unknown): asserts value is ApiError {
27 if (typeof value !== 'object' || value === null) {
28 throw new TypeError('Expected object');
29 }
30
31 const obj = value as Record<string, unknown>;
32 if (typeof obj.code !== 'string' || typeof obj.message !== 'string') {
33 throw new TypeError('Invalid API error shape');
34 }
35}Best Practices#
1// Clear error messages
2function assertPositive(n: number): asserts n is number {
3 if (n <= 0) {
4 throw new RangeError(
5 `Expected positive number, got ${n}`
6 );
7 }
8}
9
10// Document assertion behavior
11/**
12 * Asserts that value is a valid user ID.
13 * @throws {TypeError} If value is not a string
14 * @throws {Error} If value is empty or invalid format
15 */
16function assertValidUserId(value: unknown): asserts value is string {
17 // Implementation
18}
19
20// Use specific error types
21class ValidationError extends Error {
22 constructor(public field: string, message: string) {
23 super(message);
24 this.name = 'ValidationError';
25 }
26}
27
28function assertValidName(name: unknown): asserts name is string {
29 if (typeof name !== 'string') {
30 throw new ValidationError('name', 'Must be a string');
31 }
32 if (name.length < 2) {
33 throw new ValidationError('name', 'Must be at least 2 characters');
34 }
35}Conclusion#
Assertion functions validate conditions at runtime and narrow TypeScript types in a single operation. Use them when validation failure should throw an error, unlike type guards which return boolean for conditional paths. Create specific assertion functions for your domain types, use descriptive error messages, and consider custom error types for better error handling. Combine with type guards when you need both validation patterns.