2 / 3
あってはならない組み合わせは型で防ぐ
人の注意力に頼っていたルールは、注意力を持たない側がコードを書いた瞬間に崩れる。
最初
プロフィール画面の状態はフィールド三つだった。ローディング中かどうか、データ、エラー。
作った人は几帳面だった。成功したら loading を落とし、失敗したら loading を落として error を入れた。一度も漏らしたことはなかった。
type State = {
loading: boolean;
data?: User;
error?: Error;
};その間に
状態を変える場所が増えた。初回ロード、引っ張って更新、30秒ごとのポーリング、編集保存後の再取得、別タブでログアウトしたときの初期化。
五か所、五人。ある人は更新中を表示するために isRefreshing を別に作った。これで「ロード中」を意味するフィールドが二つになった。この五か所すべてが同じルールを守らなければいけないと知っているのは、最初に作った一人だけだった。
type State = {
loading: boolean;
isRefreshing: boolean;
data?: User;
error?: Error;
lastFetchedAt?: number;
};setState({ ...state, loading: true });
const data = await fetchProfile();
setState({ ...state, loading: false, data, lastFetchedAt: Date.now() });setState({ ...state, isRefreshing: true });
await refetch();
setState({ ...state, isRefreshing: false });useEffect(() => {
const id = setInterval(refetch, 30_000);
return () => clearInterval(id);
}, []);await saveProfile(form);
setState({ ...state, loading: true });
refetch();onSignOut(() => {
setState({ loading: false, isRefreshing: false });
});3か月後、六か所目
「エラーになったら再試行ボタンを出してください。」
新しく来た開発者はAIに頼んだ。AIは五か所のうち一つを参考に、catch ブロックをきれいに書き直してくれた。それらしく見えたし、型チェックも通った。
catch (e) {
setState({ ...state, error: e });
// loading は true のまま
}金曜の夜
APIサーバーが少し不安定になった。ユーザーの画面ではスピナーが回り続け、そのすぐ下にエラーメッセージと再試行ボタンが一緒に出ていた。
再試行を押すと、スピナーの下にもう一つスピナー。Slackにスクリーンショットが貼られた。
なぜこうなったか
型は「ローディング中かつエラー」という状態を許していた。フィールドが五つ、変える場所が六か所になると、二つが同時にあってはいけないというルールを毎回覚えておくのは人にも難しい。
AIにはその記憶すらなかった。許されている中から一番それらしいコードを選んだだけだ。
壊れる前に
ありうる状態だけをユニオンで列挙する。ローディング中かつエラーという状態は、もう書くことすらできない。
AIが同じ間違いをしても、今度はビルドが先に止まる。
type State =
| { status: "loading" }
| { status: "success"; data: User }
| { status: "error"; error: Error };switch (state.status) {
case "loading": return <Spinner />;
case "success": return <Profile user={state.data} />;
case "error": return <ErrorView error={state.error} />;
}