Skip to content

optics

Terminal window
bun add @stopcock/optics

Read and update deeply nested immutable data without spread hell.

Four kinds of optic, each for a different shape of access: Lens for a single value on an object, Prism for a value that might not be there (like Option or Result), Traversal for multiple values at once (every item in an array, or a filtered subset), and Iso for lossless conversion between two representations.

import { prop, view, set, over, composeLens } from '@stopcock/optics'
const name = prop<User>()('name')
const city = composeLens(prop<User>()('address'), prop<Address>()('city'))
view(name, user) // "alice"
set(city, 'London', user) // new user with city = 'London'
over(name, s => s.toUpperCase(), user)
// Manual spread: gets worse with every level of nesting
const updated = {
...state,
user: {
...state.user,
address: {
...state.user.address,
city: 'London',
},
},
}
// Optics: one line, any depth
import { path, set } from '@stopcock/optics'
const city = path<State, 'user', 'address', 'city'>('user', 'address', 'city')
const updated = set(city, 'London', state)
import { each, filtered, composeTraversal, modify } from '@stopcock/optics'
const expiredItems = composeTraversal(
each<CartItem>(),
filtered(item => item.expiresAt < Date.now()),
)
const cart = modify(expiredItems, item => ({ ...item, status: 'expired' }), state.items)
import { some, preview, composePrism } from '@stopcock/optics'
const maybeDiscount = composePrism(some<User>(), some<number>())
// Safely access user?.discount, returns Option<number>
const discount = preview(maybeDiscount, currentUser)

Focus on a single value in a product type.

type Lens<S, A> = { get: (s: S) => A; set: (a: A, s: S) => S }
lens<S, A>(get: (s: S) => A, set: (a: A, s: S) => S): Lens<S, A>
prop<S>(): <K extends keyof S>(key: K) => Lens<S, S[K]>
index<A>(i: number): Lens<A[], A>
path<S, K1 extends keyof S>(k1: K1): Lens<S, S[K1]>
view<S, A>(lens: Lens<S, A>, s: S): A
set<S, A>(lens: Lens<S, A>, a: A, s: S): S
over<S, A>(lens: Lens<S, A>, f: (a: A) => A, s: S): S
composeLens<S, A, B>(outer: Lens<S, A>, inner: Lens<A, B>): Lens<S, B>

Focus on a value that may not exist (sum types, optionals).

type Prism<S, A> = { preview: (s: S) => A | undefined; set: (a: A, s: S) => S }
prism<S, A>(preview: (s: S) => A | undefined, set: (a: A, s: S) => S): Prism<S, A>
fromPredicate<A>(pred: (a: A) => boolean): Prism<A, A>
some<A>(): Prism<A | undefined, A>
ok<A, E>(): Prism<Result<A, E>, A>
preview<S, A>(prism: Prism<S, A>, s: S): A | undefined
composePrism<S, A, B>(outer: Prism<S, A>, inner: Prism<A, B>): Prism<S, B>

Focus on multiple values at once.

type Traversal<S, A> = { toArray: (s: S) => A[]; modify: (f: (a: A) => A, s: S) => S }
traversal<S, A>(toArray: (s: S) => A[], modify: (f: (a: A) => A, s: S) => S): Traversal<S, A>
each<A>(): Traversal<A[], A>
filtered<A>(pred: (a: A) => boolean): Traversal<A[], A>
composeTraversal<S, A, B>(outer: Traversal<S, A>, inner: Traversal<A, B>): Traversal<S, B>
toArray<S, A>(traversal: Traversal<S, A>, s: S): A[]
modify<S, A>(traversal: Traversal<S, A>, f: (a: A) => A, s: S): S

Lossless bidirectional transformation.

type Iso<S, A> = { get: (s: S) => A; reverseGet: (a: A) => S }
iso<S, A>(get: (s: S) => A, reverseGet: (a: A) => S): Iso<S, A>
reverse<S, A>(iso: Iso<S, A>): Iso<A, S>
composeIso<S, A, B>(outer: Iso<S, A>, inner: Iso<A, B>): Iso<S, B>
composeOptics(outer, inner): Optic