Lens
12 exports from @ontologyio/receta/lens.
Everything exported from @ontologyio/receta/lens. New here? Start with the Lens guide.
Functions
compose
Composes two lenses to create a lens that focuses deeper into a structure.
Lens composition follows the mathematical composition pattern:
compose(outer, inner) creates a lens that first applies outer, then inner.
This allows you to build complex lenses from simple ones.
function compose<S, A, B>(outer: Lens<S, A>, inner: Lens<A, B>): Lens<S, B>interface User {
name: string
address: Address
}
interface Address {
street: string
city: string
}
const addressLens = prop<User, 'address'>('address')
const cityLens = prop<Address, 'city'>('city')
const userCityLens = compose(addressLens, cityLens)
const user = {
name: 'Alice',
address: { street: '123 Main', city: 'Boston' }
}
view(userCityLens, user) // 'Boston'
set(userCityLens, 'NYC', user)
// => { name: 'Alice', address: { street: '123 Main', city: 'NYC' } }// Multiple composition
interface State {
ui: { modal: { title: string } }
}
const uiLens = prop<State, 'ui'>('ui')
const modalLens = prop<State['ui'], 'modal'>('modal')
const titleLens = prop<State['ui']['modal'], 'title'>('title')
const modalTitleLens = compose(
compose(uiLens, modalLens),
titleLens
)// With array index: index works on readonly arrays and its focus may be undefined
interface Todo {
text: string
done: boolean
}
interface TodoList {
todos: readonly Todo[]
}
const firstTodoLens = compose(prop<TodoList, 'todos'>('todos'), index<Todo>(0))
const list: TodoList = { todos: [{ text: 'Buy milk', done: false }] }
over(firstTodoLens, todo => todo && { ...todo, done: true }, list)
// => { todos: [{ text: 'Buy milk', done: true }] }See also prop, path.
index
Creates a Lens focusing on a specific index in an array.
Allows immutable updates to array elements at a specific position.
If the index is out of bounds, get returns undefined and set has no effect.
Setting undefined has no effect either, so this lens can't remove an element.
function index<A>(idx: number): Lens<readonly A[], A | undefined>const firstLens = index<number>(0)
const thirdLens = index<number>(2)
const numbers = [1, 2, 3, 4, 5]
firstLens.get(numbers) // 1
thirdLens.get(numbers) // 3
firstLens.set(10)(numbers) // [10, 2, 3, 4, 5]
thirdLens.set(30)(numbers) // [1, 2, 30, 4, 5]// With objects in arrays
interface Todo {
id: number
text: string
done: boolean
}
const todos: Todo[] = [
{ id: 1, text: 'Buy milk', done: false },
{ id: 2, text: 'Walk dog', done: true }
]
const secondTodoLens = index<Todo>(1)
secondTodoLens.get(todos) // { id: 2, text: 'Walk dog', done: true }See also prop, path.
lens
Creates a Lens from a getter and setter function.
A lens is a composable abstraction for focusing on a specific part of a data structure. It consists of:
get: Extracts the focused value from the sourceset: Returns a new source with the focused value updated
function lens<S, A>(get: (source: S) => A, set: (value: A) => (source: S) => S): Lens<S, A>interface User {
name: string
age: number
}
const nameLens = lens<User, string>(
(user) => user.name,
(name) => (user) => ({ ...user, name })
)
const user = { name: 'Alice', age: 30 }
const newName = nameLens.get(user) // 'Alice'
const updated = nameLens.set('Bob')(user) // { name: 'Bob', age: 30 }See also prop, path.
optional
Creates a lens that handles potentially undefined values using Option.
This is useful when you have nullable fields and want to work with them safely using the Option pattern. The lens will return Some(value) if the value exists, or None if it's undefined/null.
function optional<S, A>(baseLens: Lens<S, A | undefined>): Lens<S, Option<A>>interface User {
name: string
email?: string
phone?: string
}
const emailLens = prop<User, 'email'>('email')
const optionalEmailLens = optional(emailLens)
const user1 = { name: 'Alice', email: 'alice@example.com' }
const user2 = { name: 'Bob' }
view(optionalEmailLens, user1) // Some('alice@example.com')
view(optionalEmailLens, user2) // None// Setting through optional lens
import { some, none } from '@ontologyio/receta/option'
const user = { name: 'Alice' }
set(optionalEmailLens, some('alice@example.com'), user)
// => { name: 'Alice', email: 'alice@example.com' }
set(optionalEmailLens, none(), user)
// => { name: 'Alice', email: undefined }// Transforming optional values
import { map } from '@ontologyio/receta/option'
over(
optionalEmailLens,
emailOpt => map(emailOpt, email => email.toLowerCase()),
user
)See also prop, path.
over
Applies a transformation function to the value focused by a Lens.
This is the most powerful lens operation. It reads the current value, applies a transformation, and sets the result - all immutably.
Supports both data-first and data-last (for use in pipes).
function over<S, A>(l: Lens<S, A>, fn: (a: A) => A, source: S): Sfunction over<S, A>(l: Lens<S, A>, fn: (a: A) => A): (source: S) => S// Data-first
const ageLens = prop<User, 'age'>('age')
const user = { name: 'Alice', age: 30 }
over(ageLens, age => age + 1, user)
// => { name: 'Alice', age: 31 }// Data-last (in pipe)
import * as R from 'remeda'
const updated = R.pipe(
user,
over(prop<User, 'age'>('age'), age => age + 1)
) // { name: 'Alice', age: 31 }// String manipulation
const nameLens = prop<User, 'name'>('name')
const user = { name: 'alice', age: 30 }
over(nameLens, name => name.toUpperCase(), user)
// => { name: 'ALICE', age: 30 }// Complex transformations with nested lenses
interface State {
cart: {
items: Array<{ id: string; quantity: number }>
}
}
const itemsLens = path<State, any>('cart.items')
over(
itemsLens,
items => items.map(item =>
item.id === 'abc' ? { ...item, quantity: item.quantity + 1 } : item
),
state
)// Incrementing a counter (React state pattern)
const countLens = prop<State, 'count'>('count')
setState(over(countLens, n => n + 1))See also view, set, prop.
path
Creates a Lens focusing on a nested property using dot notation.
Allows deep access into nested objects using string paths like 'user.address.city'. The lens handles immutable updates at any depth.
Paths step through plain objects and arrays, so items.0.name reaches into the
first item. The path string isn't checked against S, and the type A is taken
on trust.
function path<S, A = unknown>(pathStr: string): Lens<S, A>interface User {
name: string
address: {
street: string
city: string
zip: string
}
}
const cityLens = path<User, string>('address.city')
const user = {
name: 'Alice',
address: { street: '123 Main St', city: 'Boston', zip: '02101' }
}
cityLens.get(user) // 'Boston'
cityLens.set('NYC')(user)
// => { name: 'Alice', address: { street: '123 Main St', city: 'NYC', zip: '02101' } }// Deeply nested access
interface State {
ui: {
modal: {
isOpen: boolean
data: { title: string }
}
}
}
const modalTitleLens = path<State, string>('ui.modal.data.title')See also prop, lens.
prop
Creates a Lens focusing on a specific property of an object.
This is a convenience function for creating lenses that access a single property. It's type-safe and ensures the property exists on the object.
function prop<S, K extends keyof S>(key: K): Lens<S, S[K]>interface User {
name: string
email: string
age: number
}
const nameLens = prop<User, 'name'>('name')
const emailLens = prop<User, 'email'>('email')
const user = { name: 'Alice', email: 'alice@example.com', age: 30 }
nameLens.get(user) // 'Alice'
nameLens.set('Bob')(user) // { name: 'Bob', email: 'alice@example.com', age: 30 }See also lens, path, view, set.
set
Sets a new value through a Lens, returning an updated source object.
This creates a new object with the focused value updated, without mutating the original. The update is immutable and type-safe.
Supports both data-first and data-last (for use in pipes).
function set<S, A>(l: Lens<S, A>, value: A, source: S): Sfunction set<S, A>(l: Lens<S, A>, value: A): (source: S) => S// Data-first
const nameLens = prop<User, 'name'>('name')
const user = { name: 'Alice', age: 30 }
set(nameLens, 'Bob', user)
// => { name: 'Bob', age: 30 }
// Original user is unchanged// Data-last (in pipe)
import * as R from 'remeda'
const updated = R.pipe(
user,
set(prop<User, 'name'>('name'), 'Bob')
) // { name: 'Bob', age: 30 }// With nested lenses
const cityLens = path<User, string>('address.city')
const user = {
name: 'Alice',
address: { street: '123 Main', city: 'Boston', zip: '02101' }
}
set(cityLens, 'NYC', user)
// => {
// name: 'Alice',
// address: { street: '123 Main', city: 'NYC', zip: '02101' }
// }See also view, over, prop.
view
Gets the value focused by a Lens from a source object.
This is the primary way to read through a lens. It applies the lens's
get function to extract the focused value.
Supports both data-first and data-last (for use in pipes).
function view<S, A>(l: Lens<S, A>, source: S): Afunction view<S, A>(l: Lens<S, A>): (source: S) => A// Data-first
const nameLens = prop<User, 'name'>('name')
const user = { name: 'Alice', age: 30 }
view(nameLens, user) // 'Alice'// Data-last (in pipe)
import * as R from 'remeda'
const result = R.pipe(
user,
view(prop<User, 'name'>('name'))
) // 'Alice'// With nested lenses
const cityLens = path<User, string>('address.city')
const user = {
name: 'Alice',
address: { street: '123 Main', city: 'Boston', zip: '02101' }
}
view(cityLens, user) // 'Boston'See also set, over, prop.
Types
Lens
A Lens is a composable getter/setter pair for immutable updates.
A lens focuses on a specific part of a data structure, allowing you to read and update that part without mutating the original structure.
interface Lens<S, A> {
readonly get: (source: S) => A
readonly set: (value: A) => (source: S) => S
}// Lens focusing on a user's name
const nameLens: Lens<User, string> = {
get: (user) => user.name,
set: (name) => (user) => ({ ...user, name })
}Path
A dot-separated path to a nested property, such as 'address.city'.
This is an alias for string: the path isn't checked against T.
type Path<T> = stringconst userPath: Path<User> = 'address.city'PathValue
Type helper to extract the type at a given path.
type PathValue<T, P extends string> = P extends keyof T
? T[P]
: P extends `${infer K}.${infer Rest}`
? K extends keyof T
? PathValue<T[K], Rest>
: never
: never