TypeScript

TypeScriptでオブジェクトの部分更新する方法

概要

JavaScriptやTypeScriptでプログラムを書いていて、オブジェクトの一部のフィールドだけ更新した新しいオブジェクトを返したい、というケースが増えてきました。
これはイミュータブルなオブジェクトを使ってデータを管理する手法が流行ってきたのが大きな原因だと思います。たとえば React の props や state は、オブジェクトの一部を書き換えるのではなく、常に新しいオブジェクトを作って値を差し替える手法を採っています。

では、このようなデータを更新するのを TypeScript でどうやるのかというのを試行錯誤してきたのですが、Spread演算子とPartialを使うことで、おそらく一番楽な方法だと思われる手順を発見したので共有します。

手順

いくつかのフィールドを持ったデータの塊があるとします。
たとえば以下のようなユーザデータを考えます。

interface User {
  name: string;
  tel: string;
  age: number;
}

このデータをフォームの編集で使うことを考えます。フォームで各フィールドを編集し、編集されたデータはリポジトリという場所に保存するということにします。リポジトリにデータが保存されるとフォームに通知され、表示が更新されます。

リポジトリにユーザを保存する処理を以下のように書きます。

class Repository {
  user: User;

  updateUser(newUser: User) {
    this.user = newUser;
  }

  ...
}

updateUser()メソッドは、User型のデータを受け取り保存します。

次にupdateUser()メソッドを呼び出す部分の処理を考えてみます。
Reactを使っているなら、それぞれのフィールドごとにonChange ハンドラを作る感じになるでしょう。ユーザ名が変更されたときの処理は以下のように書けます。

  onChangeName(e: React.FormEvent<HTMLInputElement>) {
    const oldUser: User = this.props.user;
    const str: string = e.currentTarget.value;
    const newUser: User = {
      name: str,
      tel: oldUser.tel,
      age: oldUse.age
    };
    repository.updateUser(newUser);
  }

最初、↑のように書いていたのですが、各フィールドのonChangeごとにこれを書くのは辛すぎます。
幸いなことに、Spread演算子を使うことで、oldUserからのコピー部分は大きく省略することができます。

  onChangeName(e: React.FormEvent<HTMLInputElement>) {
    const oldUser: User = this.props.user;
    const str: string = e.currentTarget.value;
    const newUser: User = {...oldUser, name: str};
    repository.updateUser(newUser);
  }

さらにPartialを使うことで、onChangeハンドラの記述量を減らすことができます。

そもそも古い情報と更新したい情報のマージをonChangeハンドラで行っているのは、repository.updateUser() がUser型を受け取っているからです。
本当なら onChangeハンドラでは「どのフィールドを更新するか」という情報だけを扱って、その情報をrepository.updateUser()に渡すことができればベストです。

これを実現する方法を考えてみます。Spread演算子でUser型のオブジェクトと差分オブジェクトのマージを行って、User型のオブジェクトを得るためには、差分オブジェクトがUser型の部分集合である必要があります。

// ↓の式が成り立つには、orgUserはUser型で、partOfUserはUser型の部分集合でないといけない。
const updatedUser: User = {...orgUser, ...partOfUser}

User型の定義を再掲します。

interface User {
  name: string;
  tel: string;
  age: number;
}

このUser型の部分集合を、TypeScriptでは以下のように定義することができます。

interface UserPartial {
  name?: string;
  tel?: string;
  age?: number;
}

ありがたいことに、TypeScriptにはこのような部分集合を作るためのPartialというMapped Typeが定義されています。上で定義したUserPartial と同様の型は、以下のように表現できます。

Partial<User>

repository.updateUser() を、Partial を受け取るように書き換えてみます。

class Repository {
  user: User;

  updateUser(part: Partial<User>) {
    this.user = {...this.user, ...part};
  }
}

呼び出し元部分をこれに合わせて書き換えます。

  onChangeName(e: React.FormEvent<HTMLInputElement>) {
    const str: string = e.currentTarget.value;
    repository.updateUser({name: str});
  }

今やonChangeハンドラは元のオブジェクトを知る必要も、マージをする必要もなくなりました。
このやりかたが一番楽だと思います。

また、repository.updateUser() の引数は、User型の部分集合であることをコンパイラがチェックしてくれるため、間違ったフィールド名や型を引数に渡すとコンパイラエラーを起こしてくれます。便利。

まとめ

Partialはオブジェクトの部分更新をするのにとても便利だという知見の紹介でした。
ご参考まで。