Skip to content

Why Pura?

Pura makes Pure Functional Programming the default choice for TypeScript developers.

The Problem with Immutability

Traditional immutable updates in JavaScript are slow and verbose:

typescript
// ❌ Manual immutable update (slow, error-prone)
const updatedState = {
  ...state,
  user: {
    ...state.user,
    profile: {
      ...state.user.profile,
      settings: {
        ...state.user.profile.settings,
        theme: 'dark'
      }
    }
  }
}

// ❌ Array updates require full copies
const updatedArray = [
  ...state.items.slice(0, 500),
  newValue,
  ...state.items.slice(501)
]

Problems:

  • O(n) complexity: Full copies for every update
  • Memory inefficient: Entire structure copied
  • Error-prone: Easy to forget spread operators
  • Verbose: Deep updates require nested spreads

The Pura Solution

Returns Native JavaScript Types

Pura returns real Array/Object/Map/Set - not custom wrappers like Immutable.js:

typescript
import { produce } from '@sylphx/pura'

const state = [1, 2, 3]
const next = produce(state, draft => {
  draft.push(4)
})

// next is a real Array
next[0]              // ✅ works - real Array access
next instanceof Array // ✅ true
await api.send(next)  // ✅ works with any library

// Compare to Immutable.js:
// const state = List([1, 2, 3])
// const next = state.push(4)
// next.get(0)         // ❌ must use .get(), not [0]
// next.toArray()      // ❌ must convert for interop

Why this matters:

  • Zero learning curve - use standard JavaScript methods
  • 100% library compatible - works anywhere expecting native types
  • No conversion overhead - no .toJS() or .toArray() needed
  • Perfect TypeScript inference - types just work

Fast with Persistent Data Structures

Pura uses persistent data structures to make immutability fast:

typescript
// ✅ Pura (fast, concise, safe)
import { produceFast } from '@sylphx/pura'

const updatedState = produceFast(state, $ => {
  $.set(['user', 'profile', 'settings', 'theme'], 'dark')
})

const updatedArray = produceFast(state.items, $ => {
  $.set(500, newValue)
})

Benefits:

  • O(log n) complexity: Only copies changed path
  • Structural sharing: Unchanged parts reused
  • Type-safe: Full TypeScript inference
  • Concise: Clean, readable syntax

Performance Comparison

vs Immer

Pura is 1.06-105x faster than Immer across all scenarios:

ScenarioImmerPuraSpeedup
Sets (1K)2.31K ops/s243K ops/s105x faster 🚀
Maps (1K)2.08K ops/s25.1K ops/s12x faster 🚀
Objects (Deep)681K ops/s1.70M ops/s2.5x faster
Arrays (100)0.87M ops/s4.63M ops/s5.3x faster

Why faster?

  • Persistent structures: Arrays/Maps/Sets use HAMT/RRB-Trees (Immer only optimizes objects)
  • Helper API: produceFast() avoids proxy overhead for mutations
  • Adaptive strategy: Small collections use native (zero overhead)

vs Manual Immutability

For a 10,000-element array, updating index 500:

ApproachComplexityOperations
Manual spreadO(n)Copy 10,000 elements
PuraO(log₃₂ n)Update ~4 tree nodes

Result: Pura is faster at ~5,000+ elements.

vs Immutable.js

FeatureImmutable.jsPura
Returns native types❌ Custom List/Map wrappers✅ Real Array/Object/Map/Set
Library compatibility❌ Must convert (.toJS(), .toArray())✅ 100% compatible
Array access❌ Must use .get(index)✅ Use [index]
TypeScript⚠️ Poor inference✅ Perfect inference
Bundle size~16KB gzipped<8KB gzipped
Learning curveSteep (new API)Minimal (standard JS)

Key Features

1. Two APIs - Choose Your Style

produce() - Immer-Compatible

typescript
import { produce } from '@sylphx/pura'

const next = produce(state, draft => {
  draft.items[0] = 999        // Direct mutation
  draft.user.name = 'Jane'    // Just like Immer
})

Use when:

  • Migrating from Immer
  • Prefer mutation syntax
  • Complex nested logic

produceFast() - Optimized (Recommended)

typescript
import { produceFast } from '@sylphx/pura'

const next = produceFast(state, $ => {
  $.set(['items', 0], 999)    // Helper-based
  $.set(['user', 'name'], 'Jane')
})

Use when:

  • Maximum performance (1.06-105x faster than Immer)
  • Clear, explicit updates
  • New projects

2. Adaptive Strategy

Automatically chooses the best representation:

typescript
// Small array (< 512) → native copy (zero overhead)
const small = [1, 2, 3]
const result = produceFast(small, $ => $.push(4))
// result is a native array - no persistent structure overhead!

// Large array (>= 512) → persistent tree (structural sharing)
const large = Array.from({ length: 1000 }, (_, i) => i)
const result = produceFast(large, $ => $.set(500, 999))
// result uses RRB-Tree - only ~4 nodes copied!

You don't need to think about this - it's automatic!

3. Structural Sharing

Unchanged parts of data structures are reused, not copied:

typescript
const state = {
  users: Array.from({ length: 10000 }, (_, i) => ({ id: i, name: `User ${i}` })),
  settings: { theme: 'light' }
}

const next = produceFast(state, $ => {
  $.set(['settings', 'theme'], 'dark')
})

// Only 'settings' object is copied
// 'users' array (10,000 items) is reused unchanged!
console.log(state.users === next.users)  // true

4. Type Safety

Perfect TypeScript inference:

typescript
interface State {
  user: {
    name: string
    age: number
  }
}

const state: State = {
  user: { name: 'John', age: 30 }
}

const next = produceFast(state, $ => {
  $.set(['user', 'age'], 31)      // ✅ Type-safe
  $.set(['user', 'invalid'], 'x') // ❌ Type error!
  $.set(['user', 'age'], 'x')     // ❌ Type error!
})

// next has type State

5. Zero Dependencies

<8KB gzipped core with excellent tree-shaking:

typescript
// Only imports what you use
import { produceFast } from '@sylphx/pura'  // ~3KB
import { produce } from '@sylphx/pura'      // ~5KB

No runtime dependencies. No bloat.

When to Use Pura

✅ Perfect For

  • State management (Redux, Zustand, Jotai)
  • React/Vue/Svelte state updates
  • API responses transformation
  • Data processing pipelines
  • Migrating from Immer (drop-in replacement)

⚠️ Consider Alternatives

  • Shallow updates: Native spread is simpler

    typescript
    // For this, use native
    const next = { ...state, field: value }
  • Performance-critical hot loops: Use mutable structures, freeze after

    typescript
    // Build mutable, then freeze
    const arr = []
    for (let i = 0; i < 10000; i++) arr.push(compute(i))
    return Object.freeze(arr)
  • Already using Immer with no issues: Migration is easy, but not urgent

Philosophy

Pure FP shouldn't be a compromise. It should be the default.

Pura makes immutable updates:

  • Faster than naive mutation (structural sharing)
  • Safer than manual spreads (impossible to mutate)
  • Easier than Immutable.js (native API)
  • Lighter than alternatives (<8KB)

Pure as it should be. 🌊

Next Steps

Released under the MIT License.