Sansanさんの記事でTypeScript開発にRailway Oriented Programmingを持ち込んでエラーハンドリングする話を読んだ
DMMF本の内容のおさらいができて良い記事だった
話の内容は、よくある手続的な実装処理の中で、
- 精査した結果を単純なbooleanで返却して分岐中で例外をスローしていたり
- DBへの更新時に例外スローされたり
- 非同期処理の結果をResult型でsuccessかどうかで分岐して例外をスローしていたり
と、多様な形で表現されたエラーが混在すると、以下の課題がある
- 『関数のパイプライン』のイメージが崩れてしまう
- エラー表現が多様なので読みにくい
- try-catchのcatchでエラーを集約させる書き方だとエラーの型情報が失われてしまう(catch節の中で網羅的に考慮できているかどうかは不明)
そこでRailway Oriented Programmingの考え方を取り入れたという話
Railway Oriented Programming
1つのインプットで2つのアウトプットをするような関数(スイッチ関数)を以下の図のようにくっ付けて2レーンにする手法
手順
- 関数の出力をResult型で表現する
- 関数にResult型を入力できるようにする
- 関数を連結する
- エラーハンドリングする
まず、こういうResult型を用意する
type Failure = { errorCode: number }; type Result<Ok, Ng extends Failure> = { success: true; data: Ok; } | { success: false; error: Ng; }
そして、Result型をインプットにして、またResult型を返す関数を用意する
この時「Result型の成功データをインプットにして、またResult型を返す関数」をインジェクションできるようにしておく
function bypass< PreviousOk, PreviousNg extends Failure, NextOk, NextNg extends Failure >( func: (i: PreviousOk) => Result<NextOk, NextNg>, ): (input: Result<PreviousOk, PreviousNg>) => Result<NextOk, PreviousNg | NextNg> { return (input) => input.success ? func(input.data) : input; }
それをpipeで繋いで、最後はResult型のデータをts-pattern
のmatch関数でハンドリングする
... (result) => match(output) .with({ error: { errorCode: 400 } }, () => { ... }) .with({ error: { errorCode: 500 } }, () => { ... }) .with({ success: true }, () => {}) .exhaustive()
これで確かに網羅的にエラーがハンドリングできて良さそう