TypeScript で Result 型を作ってエラーハンドリングを簡単に
TypeScript で Result
型を定義してエラーハンドリングをしやすくしたり、抜け漏れを防ぎやすくしてみました。
やりたいこと
TypeScript でエラーハンドリングの抜け漏れ防止や開発者体験向上のために、Rust の Result
型のように try
catch
するのではなく、返値でエラーハンドリングできないかを考えました。
Rust の Result
型は以下の通りで、エラーが発生しうる処理においては、正常に処理された場合もエラーの場合も返値として返されます。そして正常時は Ok(T)
型、エラーの場合は Err(E)
型として返されます。(T
、E
はジェネリクス。TypeScript と似てますね)
Result<T, E>
is the type used for returning and propagating errors. It is an enum with the variants,Ok(T)
, representing success and containing a value, andErr(E)
, representing error and containing an error value.Functions return
Result
whenever errors are expected and recoverable. In thestd
crate,Result
is most prominently used for I/O. - std::result - Rust
上記 Rust の Result
型と同じようなことを TypeScript で実現してみたいと思います。
本記事の Result
型を使ったブラウザでのファイル読み込みの例を こちらの記事 で紹介しています。
throw
, try
catch
の課題
まず Result
型を使いたいモチベーションとして TypeScript の throw
, try
catch
によるエラーハンドリングの課題を挙げたいと思います。
関数がエラーをスローするかどうかがわからない
雑な例ですが、以下のような未成年の場合にエラーを投げるビールを入れる関数 servBeer()
があったとします。
この servBeer()
関数を別のモジュールで以下のように使用する場合、エラーハンドリングがされていなくてもリンタやコンパイラは何も言ってくれません。開発者もレビューアも servBeer()
のソースコードを見ないとエラーハンドリングする必要があるのかどうかがわかりません。
呼び出し元で servBeer()
関数の型を参照すると以下のようになっており、これを見ても servBeer()
関数がエラーをスローするのかどうかは判断できません。
JSDoc には @throws
タグがあり、エラーを投げる可能性があることを静的に知らせることは可能です。
The @throws tag allows you to document an error that a function might throw. You can include the @throws tag more than once in a single JSDoc comment.
- @throws | Use JSDoc
試しに servBeer()
関数に JSDoc を書いてみます。
呼び出し元で servBeer()
関数を参照すると以下のように表示され、エラーを投げることはわかります。
ただ TypeScript と JSDoc の二重運用は冗長なのと、JSDoc はあくまでソフト面での対応なのでコンパイラで引っ掛けるといった強制的な対応がとれません。
catch
する例外は unknown
型である
TypeScript では catch
に渡される例外は unknown
型あるいは any
型です。tsconfig.json
の compilerOptions.useUnknownInCatchVariable
あるいは compilerOptions.strict
が true
の場合 unknown
型、false
の場合 any
型になります。
Use Unknown In Catch Variables -
useUnknownInCatchVariables
In TypeScript 4.0, support was added to allow changing the type of the variable in a catch clause fromany
tounknown
.
- TypeScript: TSConfig リファレンス - すべてのTSConfigのオプションのドキュメント
JavaScript の throw
は何でも投げつけられるかつ TypeScript は関数等で throw
される例外の型まで推論してくれないので unknown
または any
型になるのはまあ納得ではあります。
以下のいずれもが例外を発生させます。
TypeScript で Result
型を作る
Result
型の定義
では TypeScript で Result
型を実装してみます。まず完成形を示します。
まず _Result
クラスがあり、それを継承した Ok
クラスと Err
クラスがあります。
_Result
クラスは isOk()
と isErr()
メソッドを持っていて、返値の型が this is Ok<T>
と this is Err<E>
となっています。これは TypeScript の this
-based type guards で if
文等でこれらのメソッドを呼ぶことで型を限定することができます。
You can use
this is Type
in the return position for methods in classes and interfaces. When mixed with a type narrowing (e.g.if
statements) the type of the target object would be narrowed to the specifiedType
.
- TypeScript: Documentation - Classes
Ok
クラスは value
プロパティで正常時の返値を、Err
クラスは error
プロパティでエラー時の返値を保持します。プライベートプロパティーとゲッターを使用して読み取り専用にしています。
プライベートプロパティは、パブリックである通常のクラスプロパティ、例えばクラスフィールドやクラスメソッドなどに対するものです。プライベートプロパティはハッシュ # 接頭辞を使用して作成され、クラスの外部から合法的に参照することはできません。これらのクラスプロパティのプライバシーカプセル化は JavaScript 自身によって強制されます。 - プライベートプロパティ - JavaScript | MDN
TypeScript の readonly
(TypeScript: Documentation - Classes) を使用してもよいのですが、Vanilla でできるものは極力 Vanilla で実装しています。
Result
型を使う
では Result
型を使って servBeer()
を書き換えてみます。
エラーを返したいときは Err
クラスを使用し、正常時は Ok
クラスに正常時の返値を入れて返します。
servBeer()
関数の型を厳密に書くと次のようにも書けます。
次に servBeer()
の呼び出し元です。
注目すべきは、変数 result
は servBeer(18)
の返値を受け取った時点では
と Result<string, Error>
型ですが、if (result.isErr())
が true
と判定される if
文のスコープでは
と Err
型に型の限定がされています。逆に else
のスコープでは
と Ok
型に型の限定がされています。
このように isOk()
または isErr()
で型を限定しないと Result
型から直接は値が取得できないため、エラーが発生しうる関数等において Result
型を返すようにしておけば、その関数の使用者にエラーハンドリングが必要であることを伝えることができます。
またエラーハンドリングをしていない場合、ぱっと見でわかるのでレビューアも気づきやすいです。
あとがき
TypeScript で Rust の Result
型のようなものを作ってみました。色々書きましたが、純粋に try
catch
より使いやすいと思います。エラーハンドリングは HTTP リクエストやデータベースアクセス等で使う機会が多いと思いますので、それらのケースでの使用例も別途紹介したいと思います。