A complete TypeScript Result<T, E> implementation providing type-safe error handling with Ok and Err variants, utility functions for transformation and extraction, and practical async usage examples.
// Result type definition using discriminated union
type Ok<T> = {
readonly _tag: 'Ok';
readonly value: T;
};
type Err<E> = {
readonly _tag: 'Err';
readonly error: E;
};
type Result<T, E> = Ok<T> | Err<E>;
// Constructor functions
const ok = <T, E = never>(value: T): Result<T, E> => ({
_tag: 'Ok',
value,
});
const err = <E, T = never>(error: E): Result<T, E> => ({
_tag: 'Err',
error,
});
// Type guards
const isOk = <T, E>(result: Result<T, E>): result is Ok<T> =>
result._tag === 'Ok';
const isErr = <T, E>(result: Result<T, E>): result is Err<E> =>
result._tag === 'Err';
// Transform the success value
const map = <T, E, U>(
result: Result<T, E>,
fn: (value: T) => U
): Result<U, E> => {
if (isOk(result)) {
return ok(fn(result.value));
}
return result;
};
// Transform with a function that returns Result (for chaining)
const flatMap = <T, E, U>(
result: Result<T, E>,
fn: (value: T) => Result<U, E>
): Result<U, E> => {
if (isOk(result)) {
return fn(result.value);
}
return result;
};
// Transform the error value
const mapErr = <T, E, F>(
result: Result<T, E>,
fn: (error: E) => F
): Result<T, F> => {
if (isErr(result)) {
return err(fn(result.error));
}
return result;
};
// Extract value with fallback
const unwrapOr = <T, E>(result: Result<T, E>, defaultValue: T): T => {
if (isOk(result)) {
return result.value;
}
return defaultValue;
};
// Extract value or throw
const unwrap = <T, E>(result: Result<T, E>): T => {
if (isOk(result)) {
return result.value;
}
throw new Error(`Called unwrap on an Err value: ${result.error}`);
};
// Match pattern for exhaustive handling
const match = <T, E, U>(
result: Result<T, E>,
handlers: {
ok: (value: T) => U;
err: (error: E) => U;
}
): U => {
if (isOk(result)) {
return handlers.ok(result.value);
}
return handlers.err(result.error);
};
// ============ Async Utilities ============
// Wrap async operations that might throw
const tryCatchAsync = async <T, E = Error>(
fn: () => Promise<T>,
mapError: (e: unknown) => E = (e) => e as E
): Promise<Result<T, E>> => {
try {
const value = await fn();
return ok(value);
} catch (e) {
return err(mapError(e));
}
};
// ============ Practical Examples ============
// Define custom error types
type ValidationError = { type: 'validation'; field: string; message: string };
type NetworkError = { type: 'network'; status: number; message: string };
type AppError = ValidationError | NetworkError;
// Validation function returning Result
const validateEmail = (email: string): Result<string, ValidationError> => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return err({
type: 'validation',
field: 'email',
message: 'Invalid email format',
});
}
return ok(email.toLowerCase().trim());
};
const validateAge = (age: number): Result<number, ValidationError> => {
if (age < 0 || age > 150) {
return err({
type: 'validation',
field: 'age',
message: 'Age must be between 0 and 150',
});
}
return ok(age);
};
// Chaining validations with flatMap
interface User {
email: string;
age: number;
}
const createUser = (
email: string,
age: number
): Result<User, ValidationError> => {
return flatMap(validateEmail(email), (validEmail) =>
map(validateAge(age), (validAge) => ({
email: validEmail,
age: validAge,
}))
);
};
// Async example: fetching user data
interface UserData {
id: number;
name: string;
email: string;
}
const fetchUser = async (id: number): Promise<Result<UserData, AppError>> => {
return tryCatchAsync(
async () => {
// Simulating API call
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
if (!response.ok) {
throw { status: response.status, message: 'Failed to fetch user' };
}
return response.json();
},
(e): AppError => {
const error = e as { status?: number; message?: string };
return {
type: 'network',
status: error.status ?? 500,
message: error.message ?? 'Unknown error',
};
}
);
};
// Using the async Result
const displayUserInfo = async (userId: number): Promise<void> => {
const userResult = await fetchUser(userId);
const message = match(userResult, {
ok: (user) => `User: ${user.name} (${user.email})`,
err: (error) => {
if (error.type === 'network') {
return `Network error (${error.status}): ${error.message}`;
}
return `Validation error in ${error.field}: ${error.message}`;
},
});
console.log(message);
};
// Demo execution
console.log('=== Validation Examples ===');
const validUser = createUser('john@example.com', 25);
console.log('Valid user:', match(validUser, {
ok: (u) => `Created: ${u.email}, age ${u.age}`,
err: (e) => `Error: ${e.message}`,
}));
const invalidUser = createUser('invalid-email', 25);
console.log('Invalid user:', match(invalidUser, {
ok: (u) => `Created: ${u.email}, age ${u.age}`,
err: (e) => `Error: ${e.message}`,
}));
// Using unwrapOr for safe defaults
const userName = unwrapOr(
map(validUser, (u) => u.email),
'anonymous@example.com'
);
console.log('Username with fallback:', userName);
console.log('\n=== Async Example ===');
displayUserInfo(1);The Result<T, E> type is a powerful pattern borrowed from languages like Rust and Haskell that makes error handling explicit and type-safe. Instead of throwing exceptions or returning null/undefined, functions return a Result that either contains a success value (Ok) or an error (Err). This forces callers to handle both cases, eliminating entire categories of runtime errors.
The implementation uses TypeScript's discriminated unions with a _tag property to distinguish between Ok and Err variants. This approach enables the compiler to narrow types automatically when you check the tag, providing excellent IDE support and compile-time safety. The readonly modifiers ensure immutability, preventing accidental mutation of result values.
The utility functions follow functional programming conventions. map transforms the success value while preserving errors unchanged—useful for data transformations. flatMap (also called chain or bind) handles the case where your transformation function itself returns a Result, preventing nested Result<Result<T, E>, E> types. This is essential for chaining multiple operations that might fail.
The match function provides exhaustive pattern matching, ensuring you handle both success and error cases. TypeScript will error if you forget to handle a case, making your error handling complete. The unwrapOr function offers a safe escape hatch when you have a sensible default value, while unwrap should be used sparingly—only when you're certain the Result is Ok.
For async operations, tryCatchAsync wraps promise-based code that might throw, converting exceptions into Result types. This is particularly valuable when working with fetch APIs or database operations. The custom error types (ValidationError, NetworkError) demonstrate how to create rich error information that helps with debugging and user feedback. Use this pattern when you want to make failure modes explicit in your API contracts, avoid exception-based control flow, or need to compose multiple fallible operations together.