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

@fuiste/optics

v2.0.0

Published

Type-safe, functional optics (lenses, prisms, traversals, and more) for immutable data

Readme

Optics

Type-safe, functional optics for immutable data in TypeScript.

  • Lens — total focus on a required value
  • Prism — partial focus on an optional or union value
  • Iso — total, invertible mapping between two types
  • Traversal — focus on zero or more values (e.g. all elements of an array)
  • Getter — read-only total focus (computed/derived values)
  • Fold — read-only multi-focus (extract without modify)

All optics compose freely via a standalone compose function. Four ergonomic combinators — guard, at, index, and each — cover the most common construction patterns.

Installation

# npm
npm install @fuiste/optics

# pnpm
pnpm add @fuiste/optics

# yarn
yarn add @fuiste/optics

# bun
bun add @fuiste/optics

Development

  • Package manager: pnpm
  • Supported Node.js: >=20.19.0

Quick start

Lens (required data)

import { Lens } from '@fuiste/optics'

type Person = {
  name: string
  age: number
  address: { street: string; city: string }
}

const nameLens = Lens<Person>().prop('name')

const person: Person = { name: 'John', age: 30, address: { street: '123', city: 'NYC' } }

nameLens.get(person) // 'John'
nameLens.set('Jane')(person) // { name: 'Jane', age: 30, address: { ... } }
nameLens.set((name) => name.toUpperCase())(person) // name == 'JOHN'

Prism (optional data)

import { Prism } from '@fuiste/optics'

type Person = {
  name: string
  address?: { street: string; city: string }
}

const addressPrism = Prism<Person>().of({
  get: (p) => p.address,
  set: (address) => (p) => ({ ...p, address }),
})

addressPrism.get({ name: 'A' }) // undefined
addressPrism.set({ street: '456', city: 'LA' })({ name: 'A' })
// => { name: 'A', address: { street: '456', city: 'LA' } }

// Functional updater — no-op when absent
addressPrism.set((addr) => ({ ...addr, city: 'LA' }))({
  name: 'A',
  address: { street: '1', city: 'NYC' },
})
// => { name: 'A', address: { street: '1', city: 'LA' } }

Iso (invertible mapping)

import { Iso } from '@fuiste/optics'

const numberString = Iso<number, string>({ to: (n) => `${n}`, from: (s) => parseInt(s, 10) })

numberString.to(42) // '42'
numberString.from('7') // 7

Traversal (multiple values)

import { Lens, compose, each } from '@fuiste/optics'

type Team = { members: string[] }

const membersLens = Lens<Team>().prop('members')
const eachMember = each<string>()

const allMembers = compose(membersLens, eachMember)

const team: Team = { members: ['Alice', 'Bob'] }

allMembers.getAll(team) // ['Alice', 'Bob']
allMembers.modify((n) => n.toUpperCase())(team)
// => { members: ['ALICE', 'BOB'] }

Getter (computed read-only value)

import { Lens, Getter, compose } from '@fuiste/optics'

type Person = { firstName: string; lastName: string }
type Team = { lead: Person }

const fullName = Getter<Person, string>((p) => `${p.firstName} ${p.lastName}`)
const leadLens = Lens<Team>().prop('lead')

const leadName = compose(leadLens, fullName)
leadName.get({ lead: { firstName: 'Alice', lastName: 'Smith' } }) // 'Alice Smith'
// leadName has no `set` — it's read-only

Fold (read-only extraction of many values)

import { Fold } from '@fuiste/optics'

const words = Fold<string, string>((s) => s.split(' '))
words.getAll('hello world') // ['hello', 'world']

Composition

All optics compose via the standalone compose(outer, inner) function. The return type is determined automatically:

| outer ∖ inner | Lens | Prism | Iso | Traversal | Getter | Fold | | ------------- | -------- | --------- | ------- | ------------- | ---------- | -------- | | Lens | Lens | Prism | Lens | Traversal | Getter | Fold | | Prism | Prism | Prism | Prism | Traversal | Fold | Fold | | Iso | Lens | Prism | Iso | Traversal | Getter | Fold | | Traversal | Traversal| Traversal | Traversal| Traversal | Fold | Fold | | Getter | Getter | Fold | Getter | Fold | Getter | Fold | | Fold | Fold | Fold | Fold | Fold | Fold | Fold |

Rules of thumb:

  • Anything with Fold → Fold (read-only is contagious)
  • Getter + partial optic → Fold; Getter + total optic → Getter
  • Traversal absorbs other writable optics → Traversal
  • Iso is transparent: the other optic's kind wins
  • Lens ∘ Lens → Lens; everything else with Prism → Prism
import { Lens, Prism, Iso, compose } from '@fuiste/optics'

type Address = { street: string; city: string }
type Person = { name: string; address?: Address }

const addressPrism = Prism<Person>().of({
  get: (p) => p.address,
  set: (address) => (p) => ({ ...p, address }),
})
const cityLens = Lens<Address>().prop('city')

// Prism ∘ Lens => Prism
const cityPrism = compose(addressPrism, cityLens)
cityPrism.get({ name: 'A', address: { street: '1', city: 'NYC' } }) // 'NYC'
cityPrism.get({ name: 'A' }) // undefined
cityPrism.set('LA')({ name: 'A' }) // unchanged (missing path is a no-op)

// Lens ∘ Iso => Lens
type Model = { count: number }
const countLens = Lens<Model>().prop('count')
const numberString = Iso<number, string>({ to: (n) => `${n}`, from: (s) => parseInt(s, 10) })
const countAsString = compose(countLens, numberString)
countAsString.get({ count: 7 }) // '7'
countAsString.set('10')({ count: 7 }) // { count: 10 }

Multi-step chains

Compose can be nested for deep paths:

import { Lens, compose, each } from '@fuiste/optics'

type Company = { employees: Array<{ name: string; role: string }> }

const employeesLens = Lens<Company>().prop('employees')
const eachEmployee = each<{ name: string; role: string }>()
const empName = Lens<{ name: string; role: string }>().prop('name')

const allEmployeeNames = compose(compose(employeesLens, eachEmployee), empName)

allEmployeeNames.getAll(company) // ['Alice', 'Bob', ...]
allEmployeeNames.modify((n) => n.toUpperCase())(company)

Combinators

guard — type-guard prism

Creates a prism from a TypeScript type guard. Much more ergonomic than manually writing Prism().of(...) for discriminated unions.

import { guard, Lens, compose } from '@fuiste/optics'

type Circle = { type: 'circle'; radius: number }
type Square = { type: 'square'; side: number }
type Shape = Circle | Square

const circlePrism = guard<Shape, Circle>((s): s is Circle => s.type === 'circle')

circlePrism.get({ type: 'circle', radius: 5 }) // { type: 'circle', radius: 5 }
circlePrism.get({ type: 'square', side: 4 }) // undefined

// Compose with a lens
const circleRadius = compose(circlePrism, Lens<Circle>().prop('radius'))
circleRadius.get({ type: 'circle', radius: 5 }) // 5
circleRadius.set(10)({ type: 'circle', radius: 5 }) // { type: 'circle', radius: 10 }

at — record key access

Creates a prism that focuses on a key in a Record<string, V>. Returns undefined when the key is absent; sets/upserts when called.

import { at, Lens, compose } from '@fuiste/optics'

type Config = { headers: Record<string, string> }

const headersLens = Lens<Config>().prop('headers')
const authHeader = at<string>('Authorization')

const configAuth = compose(headersLens, authHeader)

configAuth.get({ headers: { Authorization: 'Bearer x' } }) // 'Bearer x'
configAuth.get({ headers: {} }) // undefined
configAuth.set('Bearer y')({ headers: {} })
// => { headers: { Authorization: 'Bearer y' } }

index — array element access

Creates a prism that focuses on a single array element. get returns undefined when the index is out of bounds, and set is a no-op when the element is missing.

import { index } from '@fuiste/optics'

const second = index<number>(1)

second.get([10, 20, 30]) // 20
second.get([10]) // undefined
second.set(99)([10, 20, 30]) // [10, 99, 30]
second.set(99)([10]) // unchanged

each — array traversal

Creates a traversal over all elements of a ReadonlyArray<A>.

import { each, compose, Lens } from '@fuiste/optics'

const nums = each<number>()
nums.getAll([1, 2, 3]) // [1, 2, 3]
nums.modify((n) => n * 2)([1, 2, 3]) // [2, 4, 6]

API reference

Types

type Lens<S, A> = {
  _tag: 'lens'
  get: (s: S) => A
  set: (a: A | ((a: A) => A)) => (s: S) => S
}

type Prism<S, A> = {
  _tag: 'prism'
  get: (s: S) => A | undefined
  set: (a: A | ((a: A) => A)) => (s: S) => S
}

type Iso<S, A> = {
  _tag: 'iso'
  to: (s: S) => A
  from: (a: A) => S
}

type Traversal<S, A> = {
  _tag: 'traversal'
  getAll: (s: S) => ReadonlyArray<A>
  modify: (f: (a: A) => A) => (s: S) => S
}

type Getter<S, A> = {
  _tag: 'getter'
  get: (s: S) => A
}

type Fold<S, A> = {
  _tag: 'fold'
  getAll: (s: S) => ReadonlyArray<A>
}

type Optic<S, A> = Lens<S, A> | Prism<S, A> | Iso<S, A> | Traversal<S, A> | Getter<S, A> | Fold<S, A>

Factories

// Lens factory — use .prop to focus on a property key
Lens<S>().prop<K extends keyof S>(key: K): Lens<S, S[K]>

// Prism factory — use .of to build from get/set
Prism<S>().of<A>({ get, set }): Prism<S, A>

// Direct constructors
Iso<S, A>({ to, from }): Iso<S, A>
Traversal<S, A>({ getAll, modify }): Traversal<S, A>
Getter<S, A>(get: (s: S) => A): Getter<S, A>
Fold<S, A>(getAll: (s: S) => ReadonlyArray<A>): Fold<S, A>

Standalone functions

// Universal composition — 36 overloads, result type inferred from inputs
compose<S, A, B>(outer: Optic<S, A>, inner: Optic<A, B>): Optic<S, B>

// Type-guard prism
guard<S, A extends S>(predicate: (s: S) => s is A): Prism<S, A>

// Record key prism
at<V>(key: string): Prism<Record<string, V>, V>

// Array index prism
index<A>(idx: number): Prism<ReadonlyArray<A>, A>

// Array element traversal
each<A>(): Traversal<ReadonlyArray<A>, A>

Utility types

InferSource<O extends Optic>  // Extract the S from any optic
InferTarget<O extends Optic>  // Extract the A from any optic

Behaviour notes

  • Lens#set and Prism#set both accept a value or (a) => a updater function and return a new object. Originals are never mutated.
  • Writable optics preserve the caller's source shape in their public setter signatures for compatibility with narrowed states.
  • Prism#get may return undefined. In composed prisms, any missing outer branch results in undefined.
  • Prism#set through a composed path where an outer branch is missing is a no-op by default. Function updaters are also no-ops when missing.
  • Exception — Prism ∘ Iso: providing a concrete value materializes via the outer Prism's set even when get returns undefined, because the Iso can always construct the intermediate value. Function updaters remain a no-op when missing.
  • Traversal#modify applies the function to every focused element. For composed traversals through a missing prism branch, modify is a no-op.
  • Unchanged updates preserve reference identity whenever the library can detect that the focused value did not change.
  • Getter and Fold are read-only — they have no set or modify. Composing any optic with a read-only optic produces a read-only result.

Best practices

  • Prefer composition of small optics over one big custom getter/setter
  • Use guard for discriminated unions instead of manual Prism().of
  • Use each + compose for bulk array operations
  • Use index() for a single array element and at() for record/map keys
  • Use Getter for derived values that shouldn't be settable
  • Treat optics as pure: never mutate inputs inside set
  • For arrays, prefer each() for all elements or index(i) for one element. Lens<T[]>().prop(index) remains supported for compatibility.