# better-option

Lightweight Option type for TypeScript with generator-based composition.

## Install

**New to better-option?**

```sh
npx better-option init
```

**Or install manually:**

```sh
npm install better-option
```

## Quick Start

```ts
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](#creating-options)
- [Transforming Options](#transforming-options)
- [Extracting Values](#extracting-values)
- [Generator Composition](#generator-composition)
- [Combining Options](#combining-options)
- [Panic](#panic)
- [Serialization](#serialization)
- [API Reference](#api-reference)
- [Agents and AI](#agents--ai)
- [Acknowledgments](#acknowledgments)

## Creating Options

```ts
// 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

```ts
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); // Pipeable
```

## Extracting Values

```ts
// 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:

```ts
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`:

```ts
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

```ts
// 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.

```ts
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 Panic
```

**Why 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:

```ts
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<Option> 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

```sh
npx better-option init
```

Interactive setup that:

1. Installs the better-option package
2. Optionally fetches source code via [opensrc](https://github.com/anthropics/opensrc) for better AI context
3. Installs the adoption skill + `/adopt-better-option` command for your agent
4. Optionally launches your agent

### What the skill does

The `/adopt-better-option` command guides your AI agent through:

- Converting `T | null | undefined` to `Option<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:

```sh
# 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 folder
```

## Acknowledgments

better-option is inspired by and designed as a companion to [better-result](https://github.com/dmmulroy/better-result) by [Dillon Mulroy](https://github.com/dmmulroy). 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
