イミュータブルにオブジェクトを更新したいなぁ
イミュータブルにオブジェクトを更新したいときってあるよね。たとえば雛形のオブジェクトがあって、それをもとにいくつもオブジェクトをつくりたいとか。
僕はだいたいこんな感じでやります。
/* まずは型をつくり */
type User = {
id: string;
name: string;
hand: number[];
};
/* 基本となるオブジェクトを作成し */
const basicUser: User = {
id: '',
name: '',
hand: []
};
/* そのオブジェクトをシャローコピーして更新する */
const user1: User = {
...basicUser,
id: 'user1',
name: 'Tanaka',
hand: [0, 1, 2]
};
/* user1_2をつくるならこんな感じ */
const user1_2: User = {
...user1,
hand: [0, 1]
};
こんな感じでUserを更新する関数をつくるかなぁ……という感じなのです。再代入はしません。
ただ、スプレッド構文の問題点はシャローコピーであることです。シャローコピーはコピー元のオブジェクトの中身が変更された場合、新しく作ったオブジェクトの中身がうっかり変わってしまう可能性があるのです。
じゃあどうするかっていうと、immerに出会うまではこうしてました。
import { clone } from 'ramda';
const user1: User = {
...basicUser,
id: 'user1',
name: 'Tanaka',
hand: [0, 1, 2]
};
const user1_2: User = {
...clone(user1),
hand: [0, 1]
};
ramda.jsを使うとちゃんとディープコピーしてくれます。で、これでもいいんだけど……。
immerを使うとこんな感じで書けます。
const user1: User = produce(basicUser, (draft) => {
draft.id = 'user1';
draft.name = 'Tanaka';
draft.hand = [0, 1, 2];
});
const user1_2: User = produce(user1, (draft) => {
draft.hand = [0, 1];
});
一見、記述量が増えてる〜っていう気もする。でもこのimmerの良いところは、うっかりディープコピーを忘れないことだと思う。手癖でスプレッド構文を使ってシャローコピーしてしまってバグを起こしたことがあって、必ずcloneするようにはしているんだが抜けることがある。そしてその手癖のミスは型とかで防げない。ということでimmerを使えば必ず差分だけアップデートしたオブジェクトを返してくれるので重宝しております。
そんなわけで、immerを使うのは……今でしょ!
あ、あとTypeScriptを使っておりreadonly関係のts(2322)エラーが出るときはcastDraftを使ってあげましょう。このcastDraftに気づくまではimmerとTypeScriptは相性悪いのかな〜と勝手に思ってました。ちゃんとドキュメントを読むことが大事ですね。
const nextHand: readonly Card[] = [monster1, monster1, trainer1];
const user1: User = produce(basicUser, (draft) => {
draft.id = 'user1';
draft.name = 'TANAKA';
draft.hand = castDraft(nextHand);
});
こんな感じでTS使っているとreadonlyなオブジェクトをつくったりすると思うんだけど、それをimmerで入れようとすると型エラーが起こるのね。それをcastDraftは解消してくれるわけです。いいね!