A porting of scala monocle library to TypeScript
npm install monocle-ts

!npm downloads
(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:
``ts`
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
`ts
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
`ts
import { Lens } from 'monocle-ts'
const company = Lens.fromProp
const address = Lens.fromProp
const street = Lens.fromProp()('street')
const name = Lens.fromProp
`
compose takes two Lenses, one from A to B and another one from B to C and creates a third Lens from A toC. Therefore, after composing company, address, street and name, we obtain a Lens from Employee tostring (the street name). Now we can use this Lens issued from the composition to modify the street name using thecapitalize
function
`ts
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
`ts
import { Lens } from 'monocle-ts'
const name = Lens.fromPath
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 clearerstring
if we could zoom into the first character of a with a Lens. However, we cannot write such a Lens becauseLenses require the field they are directed at to be _mandatory_. In our case the first character of a string isstring
optional as a can be empty. So we need another abstraction that would be a sort of partial Lens, inmonocle-ts it is called an Optional.
`ts
import { Optional } from 'monocle-ts'
import { some, none } from 'fp-ts/Option'
const firstLetterOptional = new Optional
(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 fromB to C and creates a third Optional from A to C. All Lenses can be seen as Optionals where the optionalOptional
element to zoom into is always present, hence composing an and a Lens always produces an Optional.
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 < typescript@3.0.1 you have to polyfill unknown.
You can use unknown-ts as a polyfill.
)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 monocle@2.3+ you can use the following experimental modules:
- IsoLens
- Prism
- Optional
- Traversal
- At
- Ix
-
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
`ts
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
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
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
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
)
``