The Option type is a powerful tool for handling values that may or may not be present, inspired by functional programming languages like Rust and Haskell. It provides a type-safe way to avoid common errors associated with null or undefined by explicitly representing the absence of a value.
The Option<T> type represents a value that can either be Some<T> (containing a value of type T) or None (representing the absence of a value). This approach helps prevent runtime errors by forcing developers to handle both cases explicitly.
- Type Safety: Ensures that you handle both
SomeandNonecases. - Error Reduction: Avoids common bugs related to
nullorundefined. - Explicit Handling: Makes the code more readable by clearly indicating where values might be absent.
To integrate the Option type into your existing TypeScript project:
- Add the
OptionModule: Copy theOptiontype and its utility functions into a shared module or utility file. - Convert Nullable Values: Use the
fromNullablefunction to convert existing values that might benullorundefinedintoOption<T>. - Replace Null Checks: Replace traditional
nullorundefinedchecks withisSomeandisNonefunctions.
import { Option, some, none, isSome, fromNullable } from './option';
function getSomeValue(): string | null {
return Math.random() > 0.5 ? 'Hello' : null;
}
const maybeValue: Option<string> = fromNullable(getSomeValue());
if (isSome(maybeValue)) {
console.log(maybeValue.value); // Safely access the value
} else {
console.log('No value present');
}Improved code safety and readability, reducing the need for defensive null checks.
- Always handle both
SomeandNonecases. - Use utility functions like
map,flatMap, andunwrapOrto work withOptionvalues. - Avoid using
unwrapunless you are certain the value isSome, or handle the potential error.
some<T>(value: T): Creates an Option containing a value.none(): Creates an Option representing no value.
isSome<T>(option: Option<T>): Returns true if the option is Some.isNone<T>(option: Option<T>): Returns true if the option is None.
map<T, U>(option: Option<T>, fn: (value: T) => U): Applies a function to the value if Some, otherwise returns None.flatMap<T, U>(option: Option<T>, fn: (value: T) => Option<U>): Chains operations that return Option.
unwrapOr<T>(option: Option<T>, defaultValue: T): Returns the value if Some, otherwise returns the default.unwrap<T>(option: Option<T>, message?: string): Returns the value if Some, otherwise throws an OptionError.
filter<T>(option: Option<T>, predicate: (value: T) => boolean): Returns Some if the value passes the predicate, otherwise None.
orElse<T>(option: Option<T>, alternative: () => Option<T>): Returns the option if Some, otherwise computes and returns the alternative.
- Type Safety: The TypeScript compiler ensures you handle both Some and None, preventing access to absent values.
- Error Reduction: Eliminates runtime errors caused by accessing null or undefined.
- Code Clarity: Makes the intent clear—whether a value is optional—and reduces boilerplate null checks.
- Functional Programming: Encourages functional patterns like mapping and chaining, leading to cleaner, more composable code.
import {
Option,
some,
none,
isSome,
isNone,
map,
flatMap,
filter,
unwrap,
unwrapOr,
fromNullable,
and,
or,
liftA2,
matchOption,
mapWithDefault,
} from 'ts-option';
// In-memory user database
let userID = 0;
const userDB: User[] = [];
type User = {
id: number;
name: string;
email: string;
};
// Create a user with validation
function createUser(name: string, email: string): Option<User> {
if (name.trim().length < 3 || !email.includes('@')) {
return none();
}
const user = { id: ++userID, name, email };
userDB.push(user);
return some(user);
}
// Get user by email
function getUserByEmail(userEmail: string): Option<User> {
return fromNullable(userDB.find((user) => user.email === userEmail));
}
// Get user by ID
function getUserById(userId: number): Option<User> {
return fromNullable(userDB.find((user) => user.id === userId));
}
// Update user
function updateUser(user: User): Option<User> {
const index = userDB.findIndex((u) => u.id === user.id);
if (index === -1) {
return none();
}
userDB[index] = user;
return some(user);
}
// Delete user by ID
function deleteUser(userId: number): Option<User> {
const index = userDB.findIndex((u) => u.id === userId);
if (index === -1) {
return none();
}
const user = userDB.splice(index, 1)[0];
return some(user);
}
// Example usage with all Option functionalities
(() => {
// Create users
const user1 = createUser('Alice', 'alice@example.com');
const user2 = createUser('Bob', 'bob@example.com');
const invalidUser = createUser('C', 'charlie');
// Check if users exist
console.log('User1 exists:', isSome(user1)); // true
console.log('Invalid user exists:', isNone(invalidUser)); // true
// Transform user data with map
const userName = map(user1, (u) => u.name);
console.log('User1 name:', unwrapOr(userName, 'Unknown')); // 'Alice'
// Chain operations with flatMap
const updatedUser = flatMap(user1, (u) =>
updateUser({ ...u, name: 'Alicia' })
);
console.log('Updated user:', unwrapOr(updatedUser, null)); // { id: 1, name: 'Alicia', email: 'alice@example.com' }
// Filter users
const longNameUser = filter(user2, (u) => u.name.length > 3);
console.log('Long name user:', unwrapOr(longNameUser, null)); // { id: 2, name: 'Bob', email: 'bob@example.com' }
// Handle non-existent user with unwrap and error
const nonExistentUser = getUserByEmail('nonexistent@mail.com');
try {
unwrap(nonExistentUser);
} catch (e) {
console.log('Error:', (e as Error).message); // 'Attempted to unwrap a None value'
}
// Provide default with unwrapOr
const defaultUser = unwrapOr(nonExistentUser, {
id: ++userID,
name: 'Guest',
email: 'guest@example.com',
});
console.log('Default user:', defaultUser); // { id: 3, name: 'Guest', email: 'guest@example.com' }
// Combine two options with and
const userPair = and(user1, user2);
console.log('User pair:', unwrapOr(userPair, null)); // [{ id: 1, ... }, { id: 2, ... }]
// Fallback with or
const fallbackUser = or(nonExistentUser, some(defaultUser));
console.log('Fallback user:', unwrapOr(fallbackUser, null)); // { id: 3, name: 'Guest', ... }
// Combine values with liftA2
const combinedNames = liftA2(
(u1: User, u2: User) => `${u1.name} & ${u2.name}`,
user1,
user2
);
console.log('Combined names:', unwrapOr(combinedNames, 'No pair')); // 'Alicia & Bob'
// Match cases with matchOption
const matched = matchOption(
nonExistentUser,
(u) => some(`Found: ${u.name}`),
() => some('No user found')
);
console.log('Matched result:', unwrap(matched)); // 'No user found'
// Map with default value
const nameWithDefault = mapWithDefault(user1, (u) => u.name.toUpperCase(), 'N/A');
console.log('Name with default:', unwrap(nameWithDefault)); // 'ALICIA'
// Delete a user
const deletedUser = deleteUser(1);
console.log('Deleted user:', unwrapOr(deletedUser, null)); // { id: 1, name: 'Alicia', ... }
// Display current database
console.log('Current DB:', userDB); // [{ id: 2, name: 'Bob', ... }]
})();