Skip to content

Optic

import { pipe } from '@stopcock/fp'
import * as Optic from '@stopcock/fp/optic'

The five dual read and update operations appear with both shapes in the complete dual-call catalogue.

The optic module represents immutable reads and updates without hiding partiality. Its optic kinds are:

KindFocusReadWrite
Lensexactly oneyesyes
Optionalzero or oneOptionyes when present
Prismone case of a sumOptionyes when matched
Traversalzero or morecollectmodify all
Isoexactly oneyesreversible
Getterexactly oneyesno
Foldzero or morecollectno
Setterzero or morenomodify
Atkeyed optional valueOptioninsert/delete
Optic.lens(get, replace)
Optic.optional(preview, replace)
Optic.prism(preview, replace)
Optic.traversal(collect, modify)
Optic.iso(to, from)
Optic.getter(get)
Optic.fold(collect)
Optic.setter(modify)

Common focused constructors are provided:

const name = Optic.prop<User, 'name'>('name')
const first = Optic.index<User>(0)
const admin = Optic.find<User>((user) => user.role === 'admin')
const everyUser = Optic.each<User>()
const adults = Optic.filtered<User>((user) => user.age >= 18)
const preference = Optic.atKey<'theme', Theme>('theme')
const byId = Optic.at<string, User>('user-1')
Optic.view(name, user)
pipe(user, Optic.view(name))
Optic.preview(first, users) // Option<User>
pipe(users, Optic.preview(first)) // Option<User>
Optic.collect(adults, users) // readonly User[]
pipe(users, Optic.collect(adults)) // readonly User[]
Optic.set(name, user, 'Ada')
pipe(user, Optic.set(name, 'Ada'))
Optic.modify(name, user, (value) => value.toUpperCase())
pipe(
user,
Optic.modify(name, (value) => value.toUpperCase()),
)

Each operation accepts its full argument list for a direct call. Omitting the source curries the optic (and any other argument) into a pipe step:

const updated = pipe(
user,
Optic.modify(name, (value) => value.toUpperCase()),
)
const city = Optic.compose(
Optic.prop<User, 'address'>('address'),
Optic.prop<Address, 'city'>('city'),
)
Optic.view(city, user)
pipe(user, Optic.view(city))

Composition preserves the strongest valid optic kind: lens with lens remains a lens, while partial or multi-focus paths become optional or traversal optics.

For nested object paths, the fluent builder avoids manual intermediate types:

const city = Optic.optic<User>().prop('address').prop('city').value

Optic.laws exposes the standard lens get-set, set-get, and set-set checks for custom lenses.