npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@opentf/immutate

v0.1.0

Published

A lightweight, high-performance immutability library for JavaScript objects.

Readme

@opentf/immutate

Part of the Open Tech Foundation ecosystem.

🚀 A lightweight, high-performance immutability library for JavaScript objects.

✨ Features

  • Blazing Fast: Fastest in class — wins all benchmarks against immer, mutative, structura, and craft.
  • Zero Dependencies: No runtime dependencies. Pure structural-sharing engine.
  • Simple API: Write mutable-like syntax, get immutable results.
  • Structural Sharing: Only copies the changed spine — unchanged branches are reused by reference.
  • Map & Set Support: Full draft support for Map and Set — deep modifications, iteration, and structural sharing.
  • TypeScript Support: Full type safety with Draft<T> and Immutable<T> (deep readonly).
  • Async Support: immutateAsync for async recipes.

🚀 Installation

# npm
npm install @opentf/immutate

# pnpm
pnpm add @opentf/immutate

# bun
bun add @opentf/immutate

📖 Usage

immutate allows you to write code that looks like you are mutating your data, but it actually returns a new immutable version of it.

Basic Example

import { immutate } from '@opentf/immutate';

const baseState = {
  todo: 'Learn Immutate',
  done: false
};

const nextState = immutate(baseState, (draft) => {
  draft.done = true;
});

console.log(baseState.done); // false
console.log(nextState.done); // true

Complex Updates

const users = [
  { id: 1, name: 'Alice', active: true },
  { id: 2, name: 'Bob', active: false }
];

const updatedUsers = immutate(users, (draft) => {
  draft[1].active = true;
  draft.push({ id: 3, name: 'Charlie', active: true });
});

Resetting State (Return Pattern)

You can replace the entire state by returning a value from the recipe.

const initialState = { count: 0 };
const state = { count: 100 };

const resetState = immutate(state, (draft) => {
  if (shouldReset) {
    return initialState;
  }
  draft.count++;
});

Async Recipe

const nextState = await immutateAsync(state, async (draft) => {
  const data = await fetchData();
  draft.items = data;
});

TypeScript Support

Immutate provides full type safety out of the box. It uses Draft<T> to make the draft mutable inside the recipe, and Immutable<T> to enforce compile-time immutability on the returned state.

This approach provides protection without the performance penalty of runtime Object.freeze.

import { immutate, type Immutable } from '@opentf/immutate';

interface State {
  user: { name: string };
}

const state: State = { user: { name: 'John' } };

const nextState = immutate(state, (draft) => {
  draft.user.name = 'Jane'; // ✅ Draft is mutable
});

// nextState is Immutable<State>
// nextState.user.name = 'Bob'; // ❌ TS Error: Cannot assign to 'name' because it is a read-only property.

🛡️ Safety & Best Practices

To maintain peak performance, Immutate prioritizes speed over runtime checks. Follow these guidelines to ensure state integrity:

1. Never Leak the Draft

The draft object is only valid inside the recipe function. Never assign it to a variable outside the recipe.

let leaked;
immutate(state, (draft) => {
  leaked = draft; // ❌ BAD: Never do this
});
// leaked is still a "live" Proxy, but using it here can lead to memory leaks and bugs.

2. Only Mutate the Draft

Do not attempt to mutate the baseState directly while inside a recipe. Always perform your changes on the draft.

3. Return vs. Mutation

You can either mutate the draft OR return a new value. If you return a value (other than undefined), it will completely replace the state, and any mutations made to the draft will be ignored.

4. Async Caution

When using immutateAsync, ensure that you don't have multiple overlapping async recipes modifying the same state from different places, as this can lead to classic race conditions (this is a general async state rule, not specific to Immutate).

5. Date Objects

Immutate does not proxy Date objects due to internal slot limitations in JavaScript. Treat Dates as immutable primitives: instead of calling .setFullYear(), replace the property with a new Date instance.

immutate(state, (draft) => {
  // ❌ draft.date.setFullYear(2025); (Will throw TypeError)
  draft.date = new Date("2025-01-01"); // ✅ Correct
});

⚡ Benchmarks

Compared against popular immutability libraries. All libraries pass correctness verification before benchmarking. Lower avg time is better.

Environment: Bun v1.3.12 — 5,000 iterations per test (with warmup).

Deep Nested Object

Mutating a single leaf 9 levels deep: draft.a.b.c.d.e.f.g.h.i += 1

| Library | Avg Time (ms) | Perf Score | |---|---:|---:| | @opentf/immutate 🥇 | 0.00606 | 1.9x | | craft | 0.00660 | 1.8x | | structura | 0.00726 | 1.6x | | immer | 0.00919 | 1.3x | | mutative | 0.01164 | 1.0x |

Array Push (100 items)

Pushing 100 elements to an array: draft.list.push(i)

| Library | Avg Time (ms) | Perf Score | |---|---:|---:| | @opentf/immutate 🥇 | 0.05592 | 16.9x | | mutative | 0.12875 | 7.3x | | immer | 0.49668 | 1.9x | | craft | 0.54753 | 1.7x | | structura | 0.94442 | 1.0x |

Wide Object (200 keys)

Mutating 200 properties on a flat object: draft["key" + i] = i * 2

| Library | Avg Time (ms) | Perf Score | |---|---:|---:| | @opentf/immutate 🥇 | 0.04569 | 4.8x | | structura | 0.11275 | 2.0x | | mutative | 0.17016 | 1.3x | | immer | 0.19419 | 1.1x | | craft | 0.22150 | 1.0x |

Run benchmarks locally:

bun run benchmark

🔬 Feature Comparison

| Feature | immutate | immer | mutative | structura | craft | |---|:---:|:---:|:---:|:---:|:---:| | Core | | | | | | | Proxy-based draft | ✅ | ✅ | ✅ | ✅ | ✅ | | Structural sharing | ✅ | ✅ | ✅ | ✅ | ✅ | | No-change referential equality | ✅ | ✅ | ✅ | ✅ | ✅ | | Async recipe support | ✅ | ⚠️¹ | ❌ | ✅ | ❌ | | Return value from recipe | ✅ | ✅ | ✅ | ✅ | ✅ | | Patches | | | | | | | Patch generation | ❌ | ✅ | ✅ | ✅ | ✅ | | Inverse patches (undo) | ❌ | ✅ | ✅ | ✅ | ❌ | | JSON Patch (RFC 6902) | ❌ | ❌ | ✅ | ✅² | ✅ | | Apply patches separately | ❌ | ✅ | ✅ | ✅ | ✅ | | Safety & Dev Ergonomics | | | | | | | Freeze returned state | ✅⁸ | ✅ | ✅³ | ⚠️⁴ | ❌ | | Frozen input detection | ❌ | ✅ | ✅ | ❌ | ❌ | | Draft revocation after use | ❌ | ✅ | ❌ | ❌ | ❌ | | Circular reference detection | ❌ | ❌ | ⚠️⁵ | ✅ | ❌ | | Data Types | | | | | | | Plain objects & arrays | ✅ | ✅ | ✅ | ✅ | ✅ | | Map & Set support | ✅ | ✅⁶ | ✅ | ✅ | ✅ | | Class instances | ❌ | ❌ | ✅⁷ | ❌ | ❌ | | Date objects | ❌ | ❌ | ❌ | ❌ | ❌ | | Advanced | | | | | | | Curried producer | ❌ | ✅ | ✅ | ❌ | ✅ | | Current snapshot in recipe | ❌ | ✅ | ✅ | ❌ | ❌ | | Custom shallow copy / plugins | ❌ | ❌ | ✅ | ❌ | ❌ | | TypeScript generics | ✅ | ✅ | ✅ | ✅ | ✅ | | Zero runtime dependencies | ✅ | ✅ | ✅ | ✅ | ✅ |

¹ Immer discourages async inside produce; requires createDraft/finishDraft workaround. ² Structura supports standard patches via enableStandardPatches(true). ³ Mutative auto-freeze is disabled by default for performance; opt-in via enableAutoFreeze. ⁴ Structura freezes at compile-time via TypeScript only, not at runtime. ⁵ Mutative detects circular references only when enableAutoFreeze is enabled in development mode. ⁶ Immer requires calling enableMapSet() to enable Map/Set support. ⁷ Mutative supports class instances via custom mark function. ⁸ Immutate uses deep-readonly TypeScript types to enforce immutability at compile-time with zero runtime overhead.

⚖️ License

This project is licensed under the MIT License.