| title | Model Optional Values Safely with Option | ||||||
|---|---|---|---|---|---|---|---|
| id | model-optional-values-with-option | ||||||
| skillLevel | intermediate | ||||||
| applicationPatternId | domain-modeling | ||||||
| summary | Use Option<A> to explicitly represent a value that may or may not exist, eliminating null and undefined errors. | ||||||
| tags |
|
||||||
| rule |
|
||||||
| related |
|
||||||
| author | effect_website | ||||||
| lessonOrder | 6 |
Represent values that may be absent with Option<A>. Use Option.some(value) to represent a present value and Option.none() for an absent one. This creates a container that forces you to handle both possibilities.
Functions that can return a value or null/undefined are a primary source of runtime errors in TypeScript (Cannot read properties of null).
The Option type solves this by making the possibility of an absent value explicit in the type system. A function that returns Option<User> cannot be mistaken for a function that returns User. The compiler forces you to handle the None case before you can access the value inside a Some, eliminating an entire class of bugs.
A function that looks for a user in a database is a classic use case. It might find a user, or it might not. Returning an Option<User> makes this contract explicit and safe.
import { Effect, Option } from "effect";
interface User {
id: number;
name: string;
}
const users: User[] = [
{ id: 1, name: "Paul" },
{ id: 2, name: "Alex" },
];
// This function safely returns an Option, not a User or null.
const findUserById = (id: number): Option.Option<User> => {
const user = users.find((u) => u.id === id);
return Option.fromNullable(user); // A useful helper for existing APIs
};
// The caller MUST handle both cases.
const greeting = (id: number): string =>
findUserById(id).pipe(
Option.match({
onNone: () => "User not found.",
onSome: (user) => `Welcome, ${user.name}!`,
})
);
const program = Effect.gen(function* () {
yield* Effect.log(greeting(1)); // "Welcome, Paul!"
yield* Effect.log(greeting(3)); // "User not found."
});
Effect.runPromise(program);The anti-pattern is returning a nullable type (e.g., User | null or User | undefined). This relies on the discipline of every single caller to perform a null check. Forgetting even one check can introduce a runtime error.
interface User {
id: number;
name: string;
}
const users: User[] = [{ id: 1, name: "Paul" }];
// ❌ WRONG: This function's return type is less safe.
const findUserUnsafely = (id: number): User | undefined => {
return users.find((u) => u.id === id);
};
const user = findUserUnsafely(3);
// This will throw "TypeError: Cannot read properties of undefined (reading 'name')"
// because the caller forgot to check if the user exists.
console.log(`User's name is ${user.name}`);