npm package: @nodbew/zustand-evolve
Description: Declarative, strictly-typed state migrations for Zustand's persist middleware
Latest version: 1.0.0

# zustand-evolve

> Declarative, strictly-typed state migrations for Zustand's `persist` middleware.

Replace brittle `switch` statements and manual version bumping with a type-safe migration pipeline that **guarantees correctness at compile time**.

## The Problem

Zustand's `persist` middleware provides a `migrate` function and a `version` number. As your app evolves, you end up writing something like this:

```typescript
persist(store, {
  name: "app-storage",
  version: 3,
  migrate(persisted, version) {
    // Hope you got the types right...
    if (version === 0) {
      /* ... */
    }
    if (version === 1) {
      /* ... */
    }
    if (version === 2) {
      /* ... */
    }
    return persisted as CurrentState; // Unsafe cast
  },
});
```

This breaks silently. Types aren't checked between steps. Miss a version and your users get a white screen.

## The Solution

```typescript
import { createEvolver } from "zustand-evolve";
import { z } from "zod";

// Your current state — this is both the target type and the error fallback
const initialState = { theme: "dark" as const, font: "DarkModeFont" as const };

const migrationConfig = createEvolver(initialState, { logger: console.error })
  // v0 → v1: rename isDark boolean to theme string
  .step(
    (data) => z.object({ isDark: z.boolean() }).parse(data),
    (v0) => ({ theme: v0.isDark ? "dark" : "light" }) as const,
  )
  // v1 → v2: add font field derived from theme
  .step(
    (data) => z.object({ theme: z.string() }).parse(data),
    (v1) =>
      ({
        theme: v1.theme,
        font: v1.theme === "dark" ? "DarkModeFont" : "LightModeFont",
      }) as const,
  )
  .build();

// Plug directly into Zustand
persist(store, { name: "app-storage", ...migrationConfig });
```

### What you get

- **Implicit versioning** — The number of `.step()` calls determines the version automatically.
- **Strict type chaining** — The output of step N must match the input of step N+1. The compiler enforces this.
- **Final state guarantee** — `.build()` fails to compile if the last step's output doesn't match your `initialState` type.
- **Fail-safe runtime** — If any migration throws, the library catches it, reports to your logger, and returns `initialState`.

## Installation

```bash
pnpm add zustand-evolve
```

**Peer dependency:** `zustand >= 3.6.3`

## API

### `createEvolver(initialState, options?)`

Creates a new migration builder.

| Parameter        | Type                       | Description                                                        |
| ---------------- | -------------------------- | ------------------------------------------------------------------ |
| `initialState`   | `TFinalState`              | The current app state. Used as the target type and error fallback. |
| `options.logger` | `(error: unknown) => void` | Optional error reporter for failed migrations.                     |

Returns an `Evolver` builder with `.step()` and `.build()` methods.

### `.step(parser, transform)`

Adds a migration step to the pipeline.

| Parameter   | Type                         | Description                                                                  |
| ----------- | ---------------------------- | ---------------------------------------------------------------------------- |
| `parser`    | `(data: unknown) => TParsed` | Validates raw persisted data for this version. Should throw on invalid data. |
| `transform` | `(state: TParsed) => TNext`  | Maps the parsed state to the next version's shape.                           |

Returns a new `Evolver` builder with the updated type chain.

### `.build()`

Finalizes the pipeline. Compile-time error if the final step output doesn't match `initialState`.

Returns `{ version: number; migrate: (state: unknown, version: number) => TFinalState }`.

## Runtime Behavior

When Zustand calls `migrate(persistedState, version)`:

1. **Entry** — The `parser` of the step matching `version` validates the raw data.
2. **Transform chain** — The `transform` of that step and all subsequent steps run in sequence.
3. **Error fallback** — If anything throws, `logger` is called and `initialState` is returned.

Only the entry step's parser runs. Subsequent steps receive the previous transform's output directly.

## Examples

See [docs/examples.md](docs/examples.md) for:

- Basic two-step migration
- Using Zod / Valibot for parsing
- Adding fields, removing fields, renaming fields
- Handling corrupted storage gracefully

## Documentation

| Document                                | Description                                         |
| --------------------------------------- | --------------------------------------------------- |
| [Technical Specification](docs/spec.md) | Full spec with design rationale                     |
| [Architecture](docs/architecture.md)    | Internal design, type-level mechanics, runtime flow |
| [API Reference](docs/api.md)            | Detailed API docs with type signatures              |
| [Examples](docs/examples.md)            | Usage examples and migration recipes                |

## Design Principles

- **Functional** — No classes, no mutation. Pure functions and immutable data throughout.
- **Type-first** — The type system is the primary correctness mechanism. Runtime checks are the safety net.
- **Zero runtime dependencies** — Only `zustand` as a peer dependency.

## License

MIT