UNPKG

7.03 kBMarkdownView Raw
1[![build status](https://img.shields.io/travis/gcanti/monocle-ts/master.svg?style=flat-square)](https://travis-ci.org/gcanti/monocle-ts)
2[![dependency status](https://img.shields.io/david/gcanti/monocle-ts.svg?style=flat-square)](https://david-dm.org/gcanti/monocle-ts)
3![npm downloads](https://img.shields.io/npm/dm/monocle-ts.svg)
4
5# Motivation
6
7(Adapted from [monocle site](https://www.optics.dev/Monocle/))
8
9Modifying immutable nested object in JavaScript is verbose which makes code difficult to understand and reason about.
10
11Let's have a look at some examples:
12
13```ts
14interface Street {
15 num: number
16 name: string
17}
18interface Address {
19 city: string
20 street: Street
21}
22interface Company {
23 name: string
24 address: Address
25}
26interface Employee {
27 name: string
28 company: Company
29}
30```
31
32Let’s say we have an employee and we need to upper case the first character of his company street name. Here is how we
33could write it in vanilla JavaScript
34
35```ts
36const employee: Employee = {
37 name: 'john',
38 company: {
39 name: 'awesome inc',
40 address: {
41 city: 'london',
42 street: {
43 num: 23,
44 name: 'high street'
45 }
46 }
47 }
48}
49
50const capitalize = (s: string): string => s.substring(0, 1).toUpperCase() + s.substring(1)
51
52const employeeCapitalized = {
53 ...employee,
54 company: {
55 ...employee.company,
56 address: {
57 ...employee.company.address,
58 street: {
59 ...employee.company.address.street,
60 name: capitalize(employee.company.address.street.name)
61 }
62 }
63 }
64}
65```
66
67As we can see copy is not convenient to update nested objects because we need to repeat ourselves. Let's see what could
68we do with `monocle-ts`
69
70```ts
71import { Lens } from 'monocle-ts'
72
73const company = Lens.fromProp<Employee>()('company')
74const address = Lens.fromProp<Company>()('address')
75const street = Lens.fromProp<Address>()('street')
76const name = Lens.fromProp<Street>()('name')
77```
78
79`compose` takes two `Lenses`, one from `A` to `B` and another one from `B` to `C` and creates a third `Lens` from `A` to
80`C`. Therefore, after composing `company`, `address`, `street` and `name`, we obtain a `Lens` from `Employee` to
81`string` (the street name). Now we can use this `Lens` issued from the composition to modify the street name using the
82function `capitalize`
83
84```ts
85const capitalizeName = company.compose(address).compose(street).compose(name).modify(capitalize)
86
87assert.deepStrictEqual(capitalizeName(employee), employeeCapitalized)
88```
89
90You can use the `fromPath` API to avoid some boilerplate
91
92```ts
93import { Lens } from 'monocle-ts'
94
95const name = Lens.fromPath<Employee>()(['company', 'address', 'street', 'name'])
96
97const capitalizeName = name.modify(capitalize)
98
99assert.deepStrictEqual(capitalizeName(employee), employeeCapitalized) // true
100```
101
102Here `modify` lift a function `string => string` to a function `Employee => Employee`. It works but it would be clearer
103if we could zoom into the first character of a `string` with a `Lens`. However, we cannot write such a `Lens` because
104`Lenses` require the field they are directed at to be _mandatory_. In our case the first character of a `string` is
105optional as a `string` can be empty. So we need another abstraction that would be a sort of partial Lens, in
106`monocle-ts` it is called an `Optional`.
107
108```ts
109import { Optional } from 'monocle-ts'
110import { some, none } from 'fp-ts/Option'
111
112const firstLetterOptional = new Optional<string, string>(
113 (s) => (s.length > 0 ? some(s[0]) : none),
114 (a) => (s) => (s.length > 0 ? a + s.substring(1) : s)
115)
116
117const firstLetter = company.compose(address).compose(street).compose(name).asOptional().compose(firstLetterOptional)
118
119assert.deepStrictEqual(firstLetter.modify((s) => s.toUpperCase())(employee), employeeCapitalized)
120```
121
122Similarly to `compose` for lenses, `compose` for optionals takes two `Optionals`, one from `A` to `B` and another from
123`B` to `C` and creates a third `Optional` from `A` to `C`. All `Lenses` can be seen as `Optionals` where the optional
124element to zoom into is always present, hence composing an `Optional` and a `Lens` always produces an `Optional`.
125
126# TypeScript compatibility
127
128The stable version is tested against TypeScript 3.5.2, but should run with TypeScript 2.8.0+ too
129
130| `monocle-ts` version | required `typescript` version |
131| -------------------- | ----------------------------- |
132| 2.0.x+ | 3.5+ |
133| 1.x+ | 2.8.0+ |
134
135**Note**. If you are running `< typescript@3.0.1` you have to polyfill `unknown`.
136
137You can use [unknown-ts](https://github.com/gcanti/unknown-ts) as a polyfill.
138
139# Documentation
140
141- [API Reference](https://gcanti.github.io/monocle-ts/)
142
143## Experimental modules (version `2.3+`)
144
145Experimental modules (\*) are published in order to get early feedback from the community.
146
147The experimental modules are **independent and backward-incompatible** with stable ones.
148
149(\*) A feature tagged as _Experimental_ is in a high state of flux, you're at risk of it changing without notice.
150
151From `monocle@2.3+` you can use the following experimental modules:
152
153- `Iso`
154- `Lens`
155- `Prism`
156- `Optional`
157- `Traversal`
158- `At`
159- `Ix`
160
161which implement the same features contained in `index.ts` but are `pipe`-based instead of `class`-based.
162
163Here's the same examples with the new API
164
165```ts
166interface Street {
167 num: number
168 name: string
169}
170interface Address {
171 city: string
172 street: Street
173}
174interface Company {
175 name: string
176 address: Address
177}
178interface Employee {
179 name: string
180 company: Company
181}
182
183const employee: Employee = {
184 name: 'john',
185 company: {
186 name: 'awesome inc',
187 address: {
188 city: 'london',
189 street: {
190 num: 23,
191 name: 'high street'
192 }
193 }
194 }
195}
196
197const capitalize = (s: string): string => s.substring(0, 1).toUpperCase() + s.substring(1)
198
199const employeeCapitalized = {
200 ...employee,
201 company: {
202 ...employee.company,
203 address: {
204 ...employee.company.address,
205 street: {
206 ...employee.company.address.street,
207 name: capitalize(employee.company.address.street.name)
208 }
209 }
210 }
211}
212
213import * as assert from 'assert'
214import * as L from 'monocle-ts/Lens'
215import { pipe } from 'fp-ts/function'
216
217const capitalizeName = pipe(
218 L.id<Employee>(),
219 L.prop('company'),
220 L.prop('address'),
221 L.prop('street'),
222 L.prop('name'),
223 L.modify(capitalize)
224)
225
226assert.deepStrictEqual(capitalizeName(employee), employeeCapitalized)
227
228import * as O from 'monocle-ts/Optional'
229import { some, none } from 'fp-ts/Option'
230
231const firstLetterOptional: O.Optional<string, string> = {
232 getOption: (s) => (s.length > 0 ? some(s[0]) : none),
233 set: (a) => (s) => (s.length > 0 ? a + s.substring(1) : s)
234}
235
236const firstLetter = pipe(
237 L.id<Employee>(),
238 L.prop('company'),
239 L.prop('address'),
240 L.prop('street'),
241 L.prop('name'),
242 L.composeOptional(firstLetterOptional)
243)
244
245assert.deepStrictEqual(
246 pipe(
247 firstLetter,
248 O.modify((s) => s.toUpperCase())
249 )(employee),
250 employeeCapitalized
251)
252```