Skip to content

Optic

import * as Optic from '@stopcock/fp/optic'

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)
Optic.preview(first)(users) // Option<User>
Optic.collect(adults)(users) // readonly User[]
Optic.set(name, 'Ada')(user)
Optic.modify(name, (value) => value.toUpperCase())(user)

Each operation curries the optic (and any other argument) ahead of the source, so it drops straight into pipe:

import { pipe } from '@stopcock/fp'
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)

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.