状態を自然言語でインターフェイスにする
この記事では、TypeScriptの型設計において状態パラメータが増えた時に自然言語で画面の状態をおおまかに型定義して、常に有効な状態を表現できるようにすることの重要性について説明します。
基本概念
もし型設計が優れていれば、コードは書きやすくなります。しかし型設計が貧弱だと、どれだけの工夫やドキュメンテーションを施しても救えません。コードは混乱しやすく、バグが発生しやすくなります。
効果的な型設計の鍵は、有効な状態のみを表現できる型を作ることです。
例1: Webアプリケーションの状態管理
問題のある設計
interface State {
pageText: string;
isLoading: boolean;
error?: string;
}
この設計では、以下のような曖昧さが生じます:
-
isLoading
とerror
の両方が設定された場合はどうなるか? - 読み込み中のメッセージとエラーメッセージ、どちらを表示すべきか?
- ページ遷移関数で正しく状態を管理することが難しい
async function changePage(state: State, newPage: string) {
state.isLoading = true;
try {
const response = await fetch(getUrlForPage(newPage));
if (!response.ok) {
throw new Error(`Unable to load ${newPage}: ${response.statusText}`);
}
const text = await response.text();
state.isLoading = false;
state.pageText = text;
} catch (e) {
state.error = '' + e;
// isLoadingをfalseに設定し忘れている
}
}
この実装には多くの問題があります:
- エラー時に
isLoading
をfalse
に設定していない - 以前のエラーメッセージがクリアされていない
- ページ読み込み中に再度ページを変更すると、動作が予測不能になる
改善された設計
interface RequestPending {
state: 'pending';
}
interface RequestError {
state: 'error';
error: string;
}
interface RequestSuccess {
state: 'ok';
pageText: string;
}
type RequestState = RequestPending | RequestError | RequestSuccess;
interface State {
currentPage: string;
requests: {[page: string]: RequestState};
}
この設計では、タグ付き共用体(discriminated union)を使い、ネットワークリクエストの異なる状態を明示的にモデル化しています。コードは長くなりますが、無効な状態を表現できないという大きな利点があります。
function renderPage(state: State) {
const {currentPage} = state;
const requestState = state.requests[currentPage];
switch (requestState.state) {
case 'pending':
return `Loading ${currentPage}...`;
case 'error':
return `Error! Unable to load ${currentPage}: ${requestState.error}`;
case 'ok':
return `<h1>${currentPage}</h1>\n${requestState.pageText}`;
}
}
この実装では、曖昧さがなくなり、各リクエストは常に明確に一つの状態にあります。ユーザーがリクエスト発行後にページを変更しても問題ありません。
function getStickSetting(controls: CockpitControls) {
return (controls.leftSideStick + controls.rightSideStick) / 2;
}
重要なポイント
- 有効と無効の両方の状態を表現できる型は、混乱を招きエラーが発生しやすいコードにつながります
- 有効な状態のみを表現できる型を優先しましょう。表現が長くなったり難しくなったりしても、最終的には時間と労力を節約できます
- タグ付き共用体(discriminated union)を使用すると、異なる状態を明示的にモデル化できます
- 型設計の際は、どの値を含め、どの値を除外するかを慎重に考えましょう
この原則は非常に一般的なもので、型設計の多くの側面に適用できます。有効な状態のみを表現できる型を使うことで、コードはより書きやすく、TypeScriptによるチェックも容易になります。