基本的にはimmutability-helperとかlodash.setとか好きに使えばいいと思いますが、TypeSafeかつComposableにやりたいという場合のためにLensを試して見ます。
ScalaのLensライブラリであるmonocleのts版(monocle-ts)を使ってみます。
Lensとは?
以下のようにネストしたobjectをいい感じに取得したり更新できるものです。
const employee: Employee = {
name: 'john',
company: {
name: 'awesome inc',
address: {
city: 'london',
street: {
num: 23,
name: 'high street'
}
}
}
}
末端のnameにcapitalize関数をimmutableに当てたい場合、通常以下のようになります...
const capitalize = (s: string): string => s.substring(0, 1).toUpperCase() + s.substring(1)
const employee2 = {
...employee,
company: {
...employee.company,
address: {
...employee.company.address,
street: {
...employee.company.address.street,
name: capitalize(employee.company.address.street.name)
}
}
}
}
Lensを使うと、
import { Lens, Optional } from 'monocle-ts'
const company = Lens.fromProp<Employee, 'company'>('company')
const address = Lens.fromProp<Company, 'address'>('address')
const street = Lens.fromProp<Address, 'street'>('street')
const name = Lens.fromProp<Street, 'name'>('name')
company
.compose(address)
.compose(street)
.compose(name)
.modify(capitalize)(employee)
このようにimmutableに更新でき、かつ合成可能な形になっています。
使ってみる
以下のようなネストしたStateで試しみる。
interface State {
data: {
value: string,
meta: {
prop: number
}
}
}
import { Lens } from 'monocle-ts'
interface State {
data: {
value: string,
meta: {
prop: number
}
}
}
const data = Lens.fromProp<State, "data">("data")
const meta = Lens.fromProp<State["data"], "meta">('meta')
const prop = Lens.fromProp<State["data"]["meta"], "prop">('prop')
const lens = data.compose(meta).compose(prop)
const state: State = {
data: {
value: "something",
meta: {
prop: 1
}
}
}
console.log(lens.get(state))
// 1
console.log(lens.set(100)(state))
// { data: { value: 'something', meta: { prop: 100 } } }
console.log(lens.modify(prop => prop + 1)(state))
// { data: { value: 'something', meta: { prop: 2 } } }
console.log(state)
// { data: { value: 'something', meta: { prop: 1 } } }
取得に失敗する場合
例えば以下のようにnullがある可能性があると取得できずにエラーになります。
そういう場合にLensでなくPrismというものが用意されています。
import { fromNullable } from "fp-ts/lib/Option";
import { Prism } from "monocle-ts";
interface State2 {
x: { id: number } | null;
}
const xLens = Lens.fromProp<State2, "x">("x")
const nullable = new Prism<{id: number} | null, {id: number}>(fromNullable, a => a)
const idLens = Lens.fromProp<{id: number}, "id">("id")
const x = xLens.composePrism(nullable).composeLens(idLens)
console.log(x.getOption({x: {id: 1}}).getOrElseValue(0))
// 1
console.log(x.set(9)({x: {id: 1}}))
// { x: { id: 9 } }
console.log(x.set(9)({x: null}))
// { x: null }
fp-ts
monocle-tsは、fp-tsに依存してるのMonadとか好きな人にはいいと思います。
import { monoidProduct, monoidSum } from "fp-ts/lib/Monoid";
import { fromFoldable } from "monocle-ts";
import { array } from "fp-ts/lib/Array";
const xs = ["a", "bb"];
const fold = fromFoldable(array)<string>();
const len = (s: string) => s.length;
console.log(fold.foldMap(monoidSum)(len)(xs));
// 3
console.log(fold.foldMap(monoidProduct)(len)(xs));
// 2
まとめ
Lensはなかなか使う機会がないですが、stateはネストしてることが多いので使う機会があるかもと思って書いて見ましたが、まぁ普通に使わないですね