tags : TypeScript
概要
Christian Ekrem による記事。Effect-TS フレームワークを導入しなくても、TypeScript のネイティブ機能だけで関数型プログラミングのパターンを実現できるという主張。ただし Effect-TS が有効な場面も率直に認めている。
核心的な原則は「型システムに証明を持たせる(make the type system carry the proof)」という parse-don’t-validate 哲学。
問題提起:嘘をつく型シグネチャ
典型的な signupUser 関数のシグネチャ Promise<User> は、実際に起こりうる複数の失敗モードを隠蔽している。バリデーションエラー、メール重複、DB 障害、メール送信サービスのダウンなど、どれも型に反映されていない。
3つのアイデア
アイデア1:誠実なエラー(Honest Errors)
例外を throw するのではなく、エラーを判別共用体(discriminated union)として返す。
type SignupError =
| { _tag: "InvalidEmail" }
| { _tag: "EmailTaken"; email: string }
| { _tag: "DbError"; cause: unknown }
| { _tag: "EmailServiceDown" };
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };assertNever() による網羅性チェックで、呼び出し側がすべてのエラーケースを処理することをコンパイラが強制する。
アイデア2:誠実な依存関係(Honest Dependencies)
依存関係をモジュール import の裏に隠すのではなく、関数シグネチャに明示する。
type SignupDeps = {
readonly findUserByEmail: (email: string) => Promise<Result<User | null, DbError>>;
readonly createUser: (input: CreateUserInput) => Promise<Result<User, DbError>>;
readonly sendWelcomeEmail: (email: string) => Promise<Result<void, EmailError>>;
readonly trackEvent: (name: string, props: Record<string, string>) => Promise<void>;
};モックフレームワーク不要でテストしやすくなり、関数が実際に何を必要としているかが一目瞭然になる。
アイデア3:合成(Composition)
エラーチェックの繰り返しが冗長になる問題を andThen ヘルパーで解決。
async function andThen<T, U, E1, E2>(
result: Promise<Result<T, E1>>,
f: (value: T) => Promise<Result<U, E2>>,
): Promise<Result<U, E1 | E2>>ただし、4〜5個以上の逐次操作を連ねると、Effect-TS の generator 構文に比べて扱いにくくなる。
素の TypeScript の限界
- 合成の冗長性: 逐次操作が増えるとネストとボイラープレートが膨張する
- エラー型の肥大化: モジュール境界をまたぐとエラー共用体が掛け算的に増える
- 構造化された並行処理: TypeScript の Promise では、Effect の Fiber モデルが提供する安全性保証が得られない
筆者の見積もりでは、これらのパターンで日常の約80%はカバーできるが、残り20%のケースでは Effect-TS の複雑さを受け入れる価値がある。
実践的な提言
フレームワークを採用する前にこれらのパターンを手作業で学ぶことを推奨。ライブラリが「なぜ」存在するかを理解することが、便利さ以上に価値がある。