TypeScript で簡易な Result 型を定義する
TypeScript で Result 型を使いたいけど、ライブラリ入れるほどではないなーというときに見つけた記事。
エラーハンドリングの試行錯誤の結果、最終形として以下の実装が紹介されていた。
type Result<T, E> = Success<T, E> | Failure<T, E> class Success<T, E> { constructor(readonly value: T) {} type = 'success' as const // ここを追加 isSuccess(): this is Success<T, E> { return true } isFailure(): this is Failure<T, E> { return false } } class Failure<T, E> { constructor(readonly value: E) {} type = 'failure' as const // ここを追加 isSuccess(): this is Success<T, E> { return false } isFailure(): this is Failure<T, E> { return true } }
(引用ブロックにするとシンタックスハイライトが失われるのでコードブロックで引用させていただいてます)
ポイントはリテラル型の type
プロパティを定義している点で、これにより Result
がタグ付きユニオンとなるためユーザー定義型ガードの isFailure()
で型解決できるようになる。
最初は「なるほどなー」と思っていたのだが、ふと「タグ付きユニオンにするならユーザー定義型ガードも不要で、リテラル型のプロパティだけで型解決できるのでは?」という考えが浮かんだので実装してみた。
クラス版
type Result<T, E> = Success<T> | Failure<E> class Success<T> { readonly isSuccess = true readonly isFailure = false constructor(readonly value: T) {} } class Failure<E> { readonly isSuccess = false readonly isFailure = true constructor(readonly value: E) {} }
オブジェクト版
type Result<T, E> = Success<T> | Failure<E> type Success<T> = { readonly isSuccess: true readonly isFailure: false readonly value: T } type Failure<E> = { readonly isSuccess: false readonly isFailure: true readonly value: E } const createSuccess = <T>(value: T): Success<T> => { return { isSuccess: true, isFailure: false, value } } const createFailure = <E>(value: E): Failure<E> => { return { isSuccess: false, isFailure: true, value } }
オブジェクト版の方は生成時に value
以外のプロパティを書くのが手間なので、生成用の関数を用意した方が便利かなと思って書いている。
どちらも value
, isSuccess
, isFailure
のプロパティだけで構成され、よりシンプルな形になったはず(もっと攻めるならユースケースを早期リターンに限定して isSuccess
を削る手もある)。
使用例としてはこんな感じ。
function main() { const result: Result<number, string> = doSomething() if (result.isFailure) { // result: Failure<string> で型推論される } else { // result: Success<number> で型推論される } }
さらに関数型っぽく Rust の and_then
のような実装もしたくなるけど、いったんここまで。