Skip to content

Standalone API#

Note

Since optics-ts v2.2.0, there are two syntaxes for optics: method chaining and standalone optics. For more information about the differences between them, see The Two Syntaxes.

Experimental

This module is experimental and may receive backwards incompatible changes without a corresponding semver bump.

Everything below assumes the following import:

import * as O from 'optics-ts/standalone'

TypeScript types#

In general, the TypeScript types of optics look like Optic<C, A, T, R>.

  • C is the optic type as a string, e.g. 'Lens' or 'Prism'

  • A and T encode the optic's read and write direction transforms as "higher-kinded" types

  • R is true if the optic is removable, and undefined otherwise.

In the following, we leave the exact type signatures out for clarity, using a hand-wavy pseudo syntax instead. The optic descriptions try to make it clear how optics and functions operate.

Interested readers can refer to hkt.ts to see how the higher-kinded types / partially applied type operators are actually implemented.

Operations#

These function are not optics themselves, but instead operate on the optics and data.

compose#

compose :: (optic, ...optics) => Optic

Compose optics to create a more complex optic. Using a string as an optic is a shorthand for prop.

Example:

const fooBar = O.compose('foo', O.optional, 'bar')

O.preview(fooBar, { foo: undefined })
// undefined

O.preview(fooBar, { foo: { bar: 5 } })
// 5

get#

get :: (optic, source) => value
get :: (optic) => (source) => value

Read a value through an Equivalence, Iso, Lens or Getter.

Example:

O.get(O.pick('foo', 'baz'), { foo: 1, bar: 2, baz: 3 })
// { foo: 1, baz: 3 }

preview#

preview :: (optic, source) => value | undefined
preview :: (optic) => (source) => value | undefined

Read a value through a Prism, Traversal, AffineFold or Fold. For Prism and AffineFold, return undefined if the optic doesn't match (has zero focuses). For Traversal and Fold, returns the value of the first focus, or undefined if there are no focuses.

Example:

O.preview(O.optional, 1)
// 1

O.preview(O.elems, [])
// undefined

collect#

collect :: (optic, source) => value[]
collect :: (optic) => (source) => value[]

Read all focused values through a Prism, Traversal, AffineFold or Fold. For Prism and AffineFold, the return value is an array of 0 or 1 elements. For Traversal and Fold, the return value is an array of zero or more elements.

Example:

O.collect(O.optional, 1)
// [1]

O.collect(O.elems, [])
// []

modify#

modify :: (optic, fn, source) => value
modify :: (optic) => (fn, source) => value
modify :: (optic) => (fn) => (source) => value

Modify the focused value(s) through an Equivalence, Iso, Lens, Prism or Traversal. Returns an updated copy of source with all focuses modified by mapping them through the function fn.

Example:

O.modify(O.prop('foo'), (value) => value.length, { foo: 'bar' })
// { foo: 3 }

set#

Signatures:

set :: (optic, newValue, source) => value
set :: (optic) => (newValue, source) => value
set :: (optic) => (newValue) => (source) => value

Write a constant value through an Equivalence, Iso, Lens, Prism or Traversal. Returns an updated copy of source with all focuses replaced by newValue.

Example:

O.set(O.prop('foo'), null, { foo: 'bar' })
// { foo: null }

remove#

remove :: (optic, source) => value
remove :: (optic) => (source) => value

Remove the focus of a RemovablePrism from its containing container.

Example:

O.remove(O.at(1), [1, 2, 3])
// [1, 3]

Isomorphisms#

iso#

iso :: (there: (v) => u, back: (u) => v) => Iso

Create an isomorphism from functions there and back. there takes the focus and transforms it to another value. back is the inverse of there.

Note that iso is monomorphic, i.e. you cannot change the value type when writing. There's no polymorphic alternative (yet).

Example:

const sep = O.iso(
  (value: string) => value.split(','),
  (arr: string[]) => arr.join(',')
)

O.get(sep, 'foo,bar,baz')
// ['foo', 'bar', 'baz']

O.modify(sep, (arr) => [...arr].reverse(), 'foo,bar,baz')
// 'baz,bar,foo'

indexed#

indexed :: Iso

Only works on arrays.

indexed is an isomorphism from an array of values to an array of index-value pairs, i.e. from [a, b, ...] to [[0, a], [1, b], ...].

In the write direction, elements are sorted by index, and only the last one of duplicate indices are kept.

Example:

O.get(O.indexed, ['a', 'b', 'c'])
// [[0, 'a'], [1, 'b'], [2, 'c']]

O.set(O.compose(O.indexed, O.at(1), O.nth(0)), 3, ['a', 'b', 'c'])
// ['a', 'c', 'b']

Lenses#

prop#

prop :: (key) => Lens

Create a lens that focuses on the object property key.

You can also just simply pass a string to compose instead of using prop.

Example:

O.set(O.prop('foo'), 42, { foo: null })
// { foo: 42 }

See atKey for a similar prism that works on records.

nth#

nth :: (n) => Lens

Only works on tuples whose length is a least n + 1.

Create a lens that focuses on the index n of a tuple. This is a lens because the length of the focus is checked on type level, so index n is always defined.

See at for a similar prism that works on arrays.

pick#

pick :: (...keys) => Lens

Create a lens that picks the given properties from an object. When writing through the lens, you can add or remove properties.

Example:

const data = {
  foo: 'something',
  bar: 42,
  baz: true,
}

O.get(O.pick('foo', 'bar'), data)
// {
//  foo: 'something',
//  baz: true,
// }

O.set(O.pick('foo'), {}, data)
// {
//   bar: 42,
//   baz: true,
// }

O.set(O.pick(), { quux: 'added' }, data)
// {
//   foo: 'something'
//   bar: 42,
//   baz: true,
//   quux: 'added',
// }

filter#

filter :: (fn: (elem) => boolean) => Lens

Only works on arrays.

Create a lens that focuses on the array elements matched by fn. If fn is a type guard of type T, narrow the type of the focus to T[].

Writing a longer/shorter array adds/removes elements. Extraneous elements are added to the end of the array.

Example:

const l = O.filter((x: number) => x % 2 === 1)

O.set(l, ['a', 'b', 'c'], [1, 2, 3, 5, 6])
// ['a', 2, 'b', 'c', 6]

O.set(l, ['a', 'b'], [1, 2, 3, 5, 6])
// ['a', 2, 'b', 6]

O.set(l, ['a', 'b', 'c', 'd', 'e'], [1, 2, 3, 5, 6])
// ['a', 2, 'b', 'c', 6, 'd', 'e']

valueOr#

valueOr :: (defaultValue) => Lens

Create a lens that, when read through, returns defaultValue when the focused value is undefined. If the focus is not undefined, the focus is returned unchanged.

Example:

O.get(O.valueOr(0), undefined)
// 0

O.get(O.valueOr(0), 42)
// 42

O.get(O.compose('maxAge', O.valueOr(100)), { maxAge: undefined })
// 100

partsOf#

partsOf :: (traversal) => Lens

Given a traversal, create a lens that focuses on an array of the focuses of the traversal. When read through, the result is an array of elements as if produced by collect. When written through, the focuses of the traversal are replaced with the values from the written array. For a polymorphic write, the focuses of the tarversal get the type of the written array elements. If a shorter or longer array is written, throws an error. This is to ensure that all focuses are replaced and the types are correct.

Example:

O.set(
  O.partsOf(O.compose(O.elems, 'foo')),
  ['x', 'y', 'z'],
  [{ foo: 'a' }, { foo: 'b' }, { foo: 'c' }]
)
// [{ foo: 'x' }, { foo: 'y' }, { foo: 'z' }]

O.modify(O.partsOf(O.words), (words) => [...words].reverse(), 'this is a test')
// 'test a is this'

Note that composing partsOf with setters, like appendTo or prependTo, or removing elements through partsOf will not work, because the extra element added by the setter or the removed element will cause partsOf to throw.

reread, rewrite#

reread :: (fn: (value) => value) => Lens
rewrite :: (fn: (value) => value) => Lens

Create a lens that can modify the value in the read direction (reread) or write direction (rewrite). This is useful to e.g. ensure data structure invariants in some cases.

Both reread and rewrite are monomorphic.

Example:

const read = O.reread((x: string) => x.toUpperCase())
const write = O.rewrite((x: string) => x.toUpperCase())

O.get(read, 'foo')
// FOO

O.get(write, 'foo')
// foo

O.set(read, null, 'foo')
// foo

O.set(write, null, 'foo')
// FOO

lens#

lens :: (view: (v) => u, update: (v, u) => v) => Lens

Create a lens from functions view and update. view takes the current focus and returns a new focus. update takes the orginal focus and a value, and updates the original focus with that value.

Note that lens is monomorphic, i.e. you cannot change the value type when writing. There's no polymorphic alternative (yet).

Example:

const lens = O.lens<number | string, number>(
  (v) => (typeof v === 'string' ? 0 : v),
  (_, u) => u
)

O.get(lens, 100)
// 100

O.get(lens, 'foo')
// 0

Prisms#

optional#

optional :: Prism

A prism that matches if the value is not undefined. Narrows the type to remove undefined, e.g. number | undefined narrows to number.

Example:

const prism = O.compose('foo', O.optional, 'bar')

O.preview(prism, { foo: { bar: 42 } })
// 42

O.preview(prism, { foo: undefined })
// undefined

guard#

guard :: (fn: (value) => boolean) => Prism (monomorphic)
guard :: <F>() => (fn: (value) => boolean) => Prism (polymorphic)

Create a prism that matches if the value matches the type guard fn.

The first, simpler signature returns a monomorphic prism, which can only be used for writes that don't change the type of the focus.

The second signature returns a polymorphic prism that uses the type transform F to construct the result type.

Monomorphic example:

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

function isCircle(value: Shape): value is Circle {
  return value.type === 'circle'
}

const circle = { type: 'circle', radius: 42 }
const square = { type: 'square', size: 10 }

const circleRadius = O.compose(O.guard(isCircle), 'radius')

O.preview(circleRadius, square)
// undefined

O.preview(circleRadius, circle)
// 42

O.set(circleRadius, 99, square)
// { type: 'square', size: 10 }

O.set(circleRadius, 99, circle)
// { type: 'circle', radius: 99 }

Polymorphic example:

type Some<T> = { type: 'some'; value: T }
type None = { type: 'none' }
type Option<T> = Some<T> | None

// O.HKT is a "type transform" or a "higher-kinded type". this[1] is the input
// type, and the output is taken from prop 0.
//
// The SomeF type transform just checks that the input type in this[1] has the
// shape of a Some, and passes it through as-is. In other words, the writer
// can change the type of the 'value' prop, but nothing else.
//
interface SomeF extends O.HKT {
  0: this[1] extends Some<any> ? this[1] : never
}

function isSomeNumber(value: Option<number>): value is Some<number> {
  return value.type === 'some'
}

const someValue = O.compose(O.guard<SomeF>()(isSomeNumber), 'value')

const some: Option<number> = { type: 'some', value: 42 }
const none: Option<number> = { type: 'none' }

O.preview(someValue, none)
// undefined

O.preview(someValue, some)
// 42

const result1: Option<string> = O.set(someValue, 'foo', none)
// { type: 'none' }

const result2: Option<string> = O.set(someValue, 'foo', some)
// { type: 'some', value: 'foo' }

at#

at :: (i) => RemovablePrism

Only works on arrays and strings. Removable.

Create a prism that focuses on the n'th element of an array or the n'th character (substring of length 1) of a string.

When used on a string, only strings can be written. Writing strings of a different length is supported.

Example:

O.preview(O.at(1), ['a', 'b', 'c'])
// 'b'

O.preview(O.at(1), ['a'])
// undefined

O.set(O.at(1), 123, ['a', 'b', 'c'])
// ['a', 123, 'c']

O.set(O.at(1), 123, ['a'])
// ['a']

O.remove(O.at(1), ['a', 'b', 'c'])
// ['a', 'c']

O.preview(O.at(1), 'abc')
// 'b'

O.preview(O.at(1), 'a')
// undefined

O.set(O.at(1), 'X', 'abc')
// 'aXc'

O.set(O.at(1), 'X', 'a')
// 'a'

O.set(O.at(1), 'XYZ', 'abc')
// 'aXYZc'

O.set(O.at(1), '', 'abc')
// 'ac'

O.remove(O.at(1), 'abc')
// 'ac'

head :: Prism

A shorthand for at(0).

atKey#

atKey :: (key) => RemovablePrism

Only works on records (Record<string, T>). Removable.

Create a prism that focuses on the key of a record.

Example:

O.preview(O.atKey('foo'), { foo: 'bar' })
// 'bar'

O.preview(O.atKey('foo'), { hello: 'world' })
// undefined

O.set(O.atKey('foo'), 123, { foo: 'bar', hello: 'world' })
// { foo: 123, hello: 'world' }

O.set(O.atKey('foo'), 123, { hello: 'world' })
// { hello: 'world' }

O.remove(O.atKey('foo'), { foo: 'bar', hello: 'world' })
// { hello: 'world' }

find#

find :: (fn: (elem) => boolean) => RemovablePrism

Only works on arrays. Removable.

Create a prism that focuses on the first element of an array which matches the predicate fn.

Example:

const negativeElem = O.find((x: number) => x < 0)

O.preview(negativeElem, [1, 0, -1, -2])
// -1

O.modify(negativeElem, (x) => -x, [1, 0, -1, -2])
// [1, 0, 1, -2]

O.preview(negativeElem, [0, 2, 1])
// undefined

when#

when :: (fn: (value) => boolean) => Prism

Create a prism that matches it the focus matches the predicate fn. Especially useful for filtering the focuses of a travesal.

Example:

const longWords = O.compose(
  O.words,
  O.when((s: string) => s.length >= 5)
)
const text = 'Some shorter and some longer words'

O.collect(longWords, text)
// ['shorter', 'longer', 'words']

O.modify(longWords, (s) => s.toUpperCase(), text)
// "Some SHORTER and some LONGER WORDS"

Traversals#

elems#

elems :: Traversal

Only works on arrays.

A traversal that focuses on all elements of an array.

O.collect(O.compose(O.elems, 'foo', 'bar'), [
  { foo: { bar: 1 } },
  { foo: { bar: 2 } },
])
// [1, 2]

Getters#

Getters are read-only optics with a single focus. You can think of them like one-way isomorphisms or read-only lenses.

to#

to :: (fn: (v) => u) => Getter

Create a getter that applies the function fn to its focus.

Setters#

prependTo, appendTo#

prependTo :: Setter
appendTo :: Setter

Only work on arrays.

prependTo focuses on the part before the first element and appendTo focuses on the part after the last element of an array. When written through, prepends or appends the value to the array.

O.set(O.appendTo, 3, [0, 1, 2])
// [0, 1, 2, 3]

O.set(O.prependTo, 3, [0, 1, 2])
// [3, 0, 1, 2]

String traversals#

chars#

chars :: Traversal

Only works on strings.

A traversal that focuses on all the characters of a string.

When written through, characters can be removed by writing the empty string, or changed to longer strings.

O.collect(O.chars, 'foo')
// ['f', 'o', 'o']

O.modify(O.chars, (c) => (c == 'o' ? '' : c.toUpperCase()), 'foobar')
// 'FBAR'

words#

words :: Traversal

Only works on strings.

A traversal that focuses on all the words of a string. Words are substrings that are separated by whitespace.

When written through, words can be removed by writing the empty string, or changed to longer or shorter strings.

O.collect(O.words, 'foo, bar')
// ['foo,', 'bar']

O.modify(O.words, (word) => word.split('').reverse().join(''), 'foo, bar')
// ',oof rab'

Miscellaneous#

eq#

eq :: Equivalence

Equivalence is a no-op that does nothing. Acts as an identity wrt. composition.

pipe#

pipe :: (a, f1, f2, ...) => v

Pipe the value of an expression (a) into a pipeline of at most 9 unary functions (f1, f2, ...).

Example:

pipe(
  { foo: 1, bar: { baz: 2 } },
  O.set(O.prop('foo'), 3),
  O.modify(O.compose('bar', 'baz'), (v: number) => -v)
)
// { foo: 3, bar: { baz: -2 } }