TypeScript で簡易な Result 型を定義する

TypeScript で Result 型を使いたいけど、ライブラリ入れるほどではないなーというときに見つけた記事。

dev.classmethod.jp

エラーハンドリングの試行錯誤の結果、最終形として以下の実装が紹介されていた。

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 のような実装もしたくなるけど、いったんここまで。