3 / 3

状態遷移にスプレッドを使わない

型が防いでくれるのは「あってはならないものを書くこと」であって、「残っているものを引きずってくること」ではない。

ユニオンに変えた後

チームは安心した。ありえない状態はもうない。

状態を変えるコードは相変わらず手癖のまま書いた。既存の値は残し、変えるところだけ上書きする。短くて読みやすく、型チェックも通った。

useProfile.ts
setState({ ...state, status: "error", error: e });

数週間後

エラーレポートツールを入れることになった。「エラー状態になったらstateを丸ごと送ろう。デバッグしやすいように。」

同じ週に別の人がヘッダーを直した。データがあればユーザー名を表示する。

errorReporting.ts
if (state.status === "error") {
  report(state);
}
Header.tsx
if ("data" in state) {
  showUserName(state.data.name);
}

そしてある日

ログを見ていた人がおかしなことに気づいた。エラーログのたびに、直前のユーザーの個人情報が丸ごと付いていた。

同じ日、アカウント切り替え中にエラーになったユーザーのヘッダーには、前のアカウントの名前が出ていた。

実際にできあがった状態
{
  status: "error",
  error: e,
  data: 前の User,
}

なぜこうなったか

スプレッドは前の状態のフィールドを全部コピーしてくる。そしてTypeScriptは、スプレッドで入った余分なフィールドをチェックしない。

ユニオンが防いだのは「error状態にdataを書くこと」だった。すでにあったdataがついてくることまでは防げなかった。

壊れる前に

状態ごとに遷移関数を用意し、それでだけ変える。関数は必要なフィールドだけで新しいオブジェクトを作るので、引きずられるものがない。

「既存のは残してここだけ」はAIが一番よく使う手癖でもある。毎回レビューで止めるより、書ける道をなくす方が安い。

profileState.ts
const toLoading = (): State => ({ status: "loading" });
const toSuccess = (data: User): State => ({ status: "success", data });
const toError = (error: Error): State => ({ status: "error", error });
 
setState(toError(e));