better-option
v1.1.2
Published
Lightweight Option type with generator-based composition
Downloads
65
Maintainers
Readme
better-option
Lightweight Option type for TypeScript with generator-based composition.
Install
New to better-option?
npx better-option initOr install manually:
npm install better-optionQuick Start
import { Option } from "better-option";
// Convert nullable to Option
const option = Option.from(maybeNull);
// Check and use
if (Option.isSome(option)) {
console.log(option.value);
} else {
console.log("No value");
}
// Or use pattern matching
const message = option.match({
some: (data) => `Got: ${data.name}`,
none: () => "Empty",
});Contents
- Creating Options
- Transforming Options
- Extracting Values
- Generator Composition
- Combining Options
- Panic
- Serialization
- API Reference
- Agents and AI
- Acknowledgments
Creating Options
// Present value
const some = Option.some(42);
// Absent value
const none = Option.none();
// From nullable (null/undefined become None)
const option = Option.from(maybeNull);
// From predicate
const option = Option.fromPredicate(value, (x) => x > 0);Transforming Options
const option = Option.some(2)
.map((x) => x * 2) // Some(4)
.filter((x) => x > 0) // Some(4) or None
.andThen((x) => (x > 0 ? Option.some(x) : Option.none()));
// Standalone functions (data-first or data-last)
Option.map(option, (x) => x + 1);
Option.map((x) => x + 1)(option); // PipeableExtracting Values
// Unwrap (throws on None)
const value = option.unwrap();
const value = option.unwrap("custom error message");
// With fallback
const value = option.unwrapOr(defaultValue);
// Lazy fallback
const value = Option.unwrapOrElse(option, () => expensiveDefault());
// Pattern match
const value = option.match({
some: (v) => v,
none: () => fallback,
});Generator Composition
Chain multiple Options without nested callbacks or early returns:
const option = Option.gen(function* () {
const user = yield* findUser(id); // Unwraps or short-circuits
const profile = yield* findProfile(user.id);
return Option.some({ user, profile });
});
// Option<{user, profile}>Async version with Option.await:
const option = await Option.gen(async function* () {
const user = yield* Option.await(fetchUser(id));
const posts = yield* Option.await(fetchPosts(user.id));
return Option.some({ user, posts });
});When any yielded Option is None, the generator short-circuits and returns None immediately.
Combining Options
// Zip two Options into tuple
const zipped = Option.zip(optionA, optionB);
// Some([a, b]) if both Some, None otherwise
// Flatten nested Option
const flat = Option.flatten(Option.some(Option.some(42))); // Some(42)Panic
Thrown (not returned) when user callbacks throw inside Option operations. Represents a defect in your code, not a domain error.
import { Panic } from "better-option";
// Callback throws → Panic
Option.some(1).map(() => {
throw new Error("bug");
}); // throws Panic
// Generator cleanup throws → Panic
Option.gen(function* () {
try {
yield* Option.none();
} finally {
throw new Error("cleanup bug");
}
}); // throws PanicWhy Panic? None is for absent values. Panic is for bugs — like Rust's panic!(). If your .map() callback throws, that's not an expected absence, it's a defect to fix.
Panic properties:
| Property | Type | Description |
| --------- | --------- | ----------------------------- |
| message | string | Describes where/what panicked |
| cause | unknown | The exception that was thrown |
Panic also provides toJSON() for error reporting services (Sentry, etc.).
Serialization
Convert Options to plain objects for RPC, storage, or server actions:
import { Option, SerializedOption } from "better-option";
// Serialize to plain object
const option = Option.some(42);
const serialized = Option.serialize(option);
// { status: "some", value: 42 }
// Deserialize back to Option instance
const deserialized = Option.deserialize<number>(serialized);
// Some(42) - can use .map(), .andThen(), etc.
// Typed boundary for Next.js server actions
async function findUser(id: string): Promise<SerializedOption<User>> {
const option = await lookupUser(id);
return Option.serialize(option);
}
// Client-side
const serialized = await findUser(userId);
const option = Option.deserialize<User>(serialized);API Reference
Option
| Method | Description |
| ------------------------------ | ---------------------------------------- |
| Option.some(value) | Create present value |
| Option.none() | Create absent value |
| Option.from(value) | Convert nullable to Option |
| Option.fromPredicate(v, fn) | Some if predicate passes, None otherwise |
| Option.isSome(option) | Type guard for Some |
| Option.isNone(option) | Type guard for None |
| Option.filter(option, fn) | Some to None if predicate fails |
| Option.zip(a, b) | Combine two Options into tuple |
| Option.unwrapOrElse(option, fn) | Extract value or compute fallback |
| Option.gen(fn) | Generator composition |
| Option.await(promise) | Wrap Promise for generators |
| Option.serialize(option) | Convert Option to plain object |
| Option.deserialize(value) | Rehydrate serialized Option |
| Option.partition(options) | Split array into [values, noneCount] |
| Option.flatten(option) | Flatten nested Option |
Instance Methods
| Method | Description |
| ------------------------ | ------------------------------------- |
| .isSome() | Type guard, narrows to Some |
| .isNone() | Type guard, narrows to None |
| .map(fn) | Transform value |
| .filter(fn) | Some to None if predicate fails |
| .andThen(fn) | Chain Option-returning function |
| .andThenAsync(fn) | Chain async Option-returning function |
| .match({ some, none }) | Pattern match |
| .unwrap(message?) | Extract value or throw |
| .unwrapOr(fallback) | Extract value or return fallback |
| .unwrapOrElse(fn) | Extract value or compute fallback |
| .tap(fn) | Side effect on Some |
| .tapAsync(fn) | Async side effect on Some |
Type Helpers
| Type | Description |
| --------------------- | ------------------------------ |
| InferValue<O> | Extract value type from Option |
| SerializedOption<A> | Plain object form of Option |
| SerializedSome<A> | Plain object form of Some |
| SerializedNone | Plain object form of None |
Error Utilities
| Method | Description |
| -------------------- | -------------------------- |
| panic(message, c?) | Throw unrecoverable Panic |
| isPanic(value) | Type guard for Panic |
Agents and AI
better-option ships with skills for AI coding agents (OpenCode, Claude Code, Codex).
Quick Start
npx better-option initInteractive setup that:
- Installs the better-option package
- Optionally fetches source code via opensrc for better AI context
- Installs the adoption skill +
/adopt-better-optioncommand for your agent - Optionally launches your agent
What the skill does
The /adopt-better-option command guides your AI agent through:
- Converting
T | null | undefinedtoOption<T> - Migrating null checks to Option combinators
- Refactoring nullable chains to generator composition
- Replacing
array.find()patterns with Option returns
Supported agents
| Agent | Config detected | Skill location |
| -------- | ----------------------- | -------------------------------------- |
| OpenCode | .opencode/ | .opencode/skill/better-option-adopt/ |
| Claude | .claude/, CLAUDE.md | .claude/skills/better-option-adopt/ |
| Codex | .codex/, AGENTS.md | .codex/skills/better-option-adopt/ |
Manual usage
If you prefer not to use the interactive CLI:
# Install package
npm install better-option
# Add source for AI context (optional)
npx opensrc better-option
# Then copy skills/ directory to your agent's skill folderAcknowledgments
better-option is inspired by and designed as a companion to better-result by Dillon Mulroy. The project structure, API design patterns, and generator-based composition approach are all influenced by better-result's excellent work on typed error handling in TypeScript.
If you're looking for typed error handling (Result types), check out better-result!
License
MIT
