gcanti/monocle-ts
gcanti/monocle-ts has 1,073 GitHub stars and 55 forks. GitHot measured 0 stars in 24 hours. Last checked 2026-11-09 10:38 UTC. Last push 2022-12-09.
Functional optics: a (partial) porting of Scala monocle
Measured star growth
- in 24h
- 0
- in 7 days
- Update delayed
- in 15 days
- Update delayed
- in 1 month
- Update delayed
Net star changes over overlapping periods, not daily totals. Missing measurements are left blank. Stars measure attention, not software quality.
README.md
OriginalMotivation
(Adapted from monocle site)
Modifying immutable nested object in JavaScript is verbose which makes code difficult to understand and reason about.
Let's have a look at some examples:
interface Street {
num: number
name: string
}
interface Address {
city: string
street: Street
}
interface Company {
name: string
address: Address
}
interface Employee {
name: string
company: Company
}
Let’s say we have an employee and we need to upper case the first character of his company street name. Here is how we could write it in vanilla JavaScript
const employee: Employee = {
name: 'john',
company: {
name: 'awesome inc',
address: {
city: 'london',
street: {
num: 23,
name: 'high street'
}
}
}
}
const capitalize = (s: string): string => s.substring(0, 1).toUpperCase() + s.substring(1)
const employeeCapitalized = {
...employee,
company: {
...employee.company,
address: {
...employee.company.address,
street: {
...employee.company.address.street,
name: capitalize(employee.company.address.street.name)
}
}
}
}
As we can see copy is not convenient to update nested objects because we need to repeat ourselves. Let's see what could
we do with monocle-ts
import { Lens } from 'monocle-ts'
const company = Lens.fromProp<Employee>()('company')
const address = Lens.fromProp<Company>()('address')
const street = Lens.fromProp<Address>()('street')
const name = Lens.fromProp<Street>()('name')
compose takes two Lenses, one from A to B and another one from B to C and creates a third Lens from A to
C. Therefore, after composing company, address, street and name, we obtain a Lens from Employee to
string (the street name). Now we can use this Lens issued from the composition to modify the street name using the
function capitalize
const capitalizeName = company.compose(address).compose(street).compose(name).modify(capitalize)
assert.deepStrictEqual(capitalizeName(employee), employeeCapitalized)
You can use the fromPath API to avoid some boilerplate
import { Lens } from 'monocle-ts'
const name = Lens.fromPath<Employee>()(['company', 'address', 'street', 'name'])
const capitalizeName = name.modify(capitalize)
assert.deepStrictEqual(capitalizeName(employee), employeeCapitalized) // true
Here modify lift a function string => string to a function Employee => Employee. It works but it would be clearer
if we could zoom into the first character of a string with a Lens. However, we cannot write such a Lens because
Lenses require the field they are directed at to be mandatory. In our case the first character of a string is
optional as a string can be empty. So we need another abstraction that would be a sort of partial Lens, in
monocle-ts it is called an Optional.
import { Optional } from 'monocle-ts'
import { some, none } from 'fp-ts/Option'
const firstLetterOptional = new Optional<string, string>(
(s) => (s.length > 0 ? some(s[0]) : none),
(a) => (s) => (s.length > 0 ? a + s.substring(1) : s)
)
const firstLetter = company.compose(address).compose(street).compose(name).asOptional().compose(firstLetterOptional)
assert.deepStrictEqual(firstLetter.modify((s) => s.toUpperCase())(employee), employeeCapitalized)
Similarly to compose for lenses, compose for optionals takes two Optionals, one from A to B and another from
B to C and creates a third Optional from A to C. All Lenses can be seen as Optionals where the optional
element to zoom into is always present, hence composing an Optional and a Lens always produces an Optional.
TypeScript compatibility
The stable version is tested against TypeScript 3.5.2, but should run with TypeScript 2.8.0+ too
monocle-ts version |
required typescript version |
|---|---|
| 2.0.x+ | 3.5+ |
| 1.x+ | 2.8.0+ |
Note. If you are running < [email protected] you have to polyfill unknown.
You can use unknown-ts as a polyfill.
Documentation
Experimental modules (version 2.3+)
Experimental modules (*) are published in order to get early feedback from the community.
The experimental modules are independent and backward-incompatible with stable ones.
(*) A feature tagged as Experimental is in a high state of flux, you're at risk of it changing without notice.
From [email protected]+ you can use the following experimental modules:
IsoLensPrismOptionalTraversalAtIx
which implement the same features contained in index.ts but are pipe-based instead of class-based.
Here's the same examples with the new API
interface Street {
num: number
name: string
}
interface Address {
city: string
street: Street
}
interface Company {
name: string
address: Address
}
interface Employee {
name: string
company: Company
}
const employee: Employee = {
name: 'john',
company: {
name: 'awesome inc',
address: {
city: 'london',
street: {
num: 23,
name: 'high street'
}
}
}
}
const capitalize = (s: string): string => s.substring(0, 1).toUpperCase() + s.substring(1)
const employeeCapitalized = {
...employee,
company: {
...employee.company,
address: {
...employee.company.address,
street: {
...employee.company.address.street,
name: capitalize(employee.company.address.street.name)
}
}
}
}
import * as assert from 'assert'
import * as L from 'monocle-ts/Lens'
import { pipe } from 'fp-ts/function'
const capitalizeName = pipe(
L.id<Employee>(),
L.prop('company'),
L.prop('address'),
L.prop('street'),
L.prop('name'),
L.modify(capitalize)
)
assert.deepStrictEqual(capitalizeName(employee), employeeCapitalized)
import * as O from 'monocle-ts/Optional'
import { some, none } from 'fp-ts/Option'
const firstLetterOptional: O.Optional<string, string> = {
getOption: (s) => (s.length > 0 ? some(s[0]) : none),
set: (a) => (s) => (s.length > 0 ? a + s.substring(1) : s)
}
const firstLetter = pipe(
L.id<Employee>(),
L.prop('company'),
L.prop('address'),
L.prop('street'),
L.prop('name'),
L.composeOptional(firstLetterOptional)
)
assert.deepStrictEqual(
pipe(
firstLetter,
O.modify((s) => s.toUpperCase())
)(employee),
employeeCapitalized
)
Explore related projects
codecrafters-io/build-your-own-x
Master programming by recreating your favorite technologies from scratch.
561,398 starspublic-apis/public-apis
A collective list of free APIs
497,679 starsmorluto/rea
Reverse engineer anything with agents, from app behavior down to native binaries.
447,495 starsopenclaw/openclaw
The AI that really does things. Any OS. Any Platform. The lobster way. 🦞
394,261 stars