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 |
|
9 | Modifying immutable nested object in JavaScript is verbose which makes code difficult to understand and reason about.
|
10 |
|
11 | Let's have a look at some examples:
|
12 |
|
13 | ```ts
|
14 | interface Street {
|
15 | num: number
|
16 | name: string
|
17 | }
|
18 | interface Address {
|
19 | city: string
|
20 | street: Street
|
21 | }
|
22 | interface Company {
|
23 | name: string
|
24 | address: Address
|
25 | }
|
26 | interface Employee {
|
27 | name: string
|
28 | company: Company
|
29 | }
|
30 | ```
|
31 |
|
32 | 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
|
33 | could write it in vanilla JavaScript
|
34 |
|
35 | ```ts
|
36 | const 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 |
|
50 | const capitalize = (s: string): string => s.substring(0, 1).toUpperCase() + s.substring(1)
|
51 |
|
52 | const 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 |
|
67 | As we can see copy is not convenient to update nested objects because we need to repeat ourselves. Let's see what could
|
68 | we do with `monocle-ts`
|
69 |
|
70 | ```ts
|
71 | import { Lens } from 'monocle-ts'
|
72 |
|
73 | const company = Lens.fromProp<Employee>()('company')
|
74 | const address = Lens.fromProp<Company>()('address')
|
75 | const street = Lens.fromProp<Address>()('street')
|
76 | const 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
|
82 | function `capitalize`
|
83 |
|
84 | ```ts
|
85 | const capitalizeName = company.compose(address).compose(street).compose(name).modify(capitalize)
|
86 |
|
87 | assert.deepStrictEqual(capitalizeName(employee), employeeCapitalized)
|
88 | ```
|
89 |
|
90 | You can use the `fromPath` API to avoid some boilerplate
|
91 |
|
92 | ```ts
|
93 | import { Lens } from 'monocle-ts'
|
94 |
|
95 | const name = Lens.fromPath<Employee>()(['company', 'address', 'street', 'name'])
|
96 |
|
97 | const capitalizeName = name.modify(capitalize)
|
98 |
|
99 | assert.deepStrictEqual(capitalizeName(employee), employeeCapitalized) // true
|
100 | ```
|
101 |
|
102 | Here `modify` lift a function `string => string` to a function `Employee => Employee`. It works but it would be clearer
|
103 | if 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
|
105 | optional 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
|
109 | import { Optional } from 'monocle-ts'
|
110 | import { some, none } from 'fp-ts/Option'
|
111 |
|
112 | const 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 |
|
117 | const firstLetter = company.compose(address).compose(street).compose(name).asOptional().compose(firstLetterOptional)
|
118 |
|
119 | assert.deepStrictEqual(firstLetter.modify((s) => s.toUpperCase())(employee), employeeCapitalized)
|
120 | ```
|
121 |
|
122 | Similarly 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
|
124 | element to zoom into is always present, hence composing an `Optional` and a `Lens` always produces an `Optional`.
|
125 |
|
126 | # TypeScript compatibility
|
127 |
|
128 | The 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 |
|
137 | You 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 |
|
145 | Experimental modules (\*) are published in order to get early feedback from the community.
|
146 |
|
147 | The 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 |
|
151 | From `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 |
|
161 | which implement the same features contained in `index.ts` but are `pipe`-based instead of `class`-based.
|
162 |
|
163 | Here's the same examples with the new API
|
164 |
|
165 | ```ts
|
166 | interface Street {
|
167 | num: number
|
168 | name: string
|
169 | }
|
170 | interface Address {
|
171 | city: string
|
172 | street: Street
|
173 | }
|
174 | interface Company {
|
175 | name: string
|
176 | address: Address
|
177 | }
|
178 | interface Employee {
|
179 | name: string
|
180 | company: Company
|
181 | }
|
182 |
|
183 | const 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 |
|
197 | const capitalize = (s: string): string => s.substring(0, 1).toUpperCase() + s.substring(1)
|
198 |
|
199 | const 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 |
|
213 | import * as assert from 'assert'
|
214 | import * as L from 'monocle-ts/Lens'
|
215 | import { pipe } from 'fp-ts/function'
|
216 |
|
217 | const 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 |
|
226 | assert.deepStrictEqual(capitalizeName(employee), employeeCapitalized)
|
227 |
|
228 | import * as O from 'monocle-ts/Optional'
|
229 | import { some, none } from 'fp-ts/Option'
|
230 |
|
231 | const 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 |
|
236 | const 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 |
|
245 | assert.deepStrictEqual(
|
246 | pipe(
|
247 | firstLetter,
|
248 | O.modify((s) => s.toUpperCase())
|
249 | )(employee),
|
250 | employeeCapitalized
|
251 | )
|
252 | ```
|