TypeScriptでKotlinのrunCatchingを実装する

Kotlinでは try-catch の代わりに runCatching という選択肢があるのですが、これが非常に便利な仕組みなので雑に TypeScriptで実装してみます。

runCatching について知らない方は こちらの方の記事 が参考になりそうなので、ご一読を。

モチベーション

runCatching の方が綺麗に書けることがあるので、typescriptに欲しい

runCatchingの実行結果オブジェクトを実装する

runCatchingを実行したときに実行結果が格納されるクラス Result<T, E = Error> を実装します。 javascriptにはKotlinには無い undefined が存在するので、 getOrUndefined という関数も実装しておきます。 getOrElse は変数または関数を渡せるようにオーバーロードしておきます。

export class Result<T, E = Error> {
  private constructor(
    public readonly isSuccess: boolean,
    public readonly value?: T,
    public readonly error?: E
  ) {}

  static success<T, E = Error>(value: T): Result<T, E> {
    return new Result<T, E>(true, value)
  }

  static failure<T, E = Error>(error: E): Result<T, E> {
    return new Result<T, E>(false, undefined, error)
  }

  getOrNull(): T | null {
    return this.isSuccess ? this.value! : null
  }

  getOrUndefined(): T | undefined {
    return this.isSuccess ? this.value : undefined
  }

  getOrElse(defaultValue: T): T;
  getOrElse(onFailure: (error: E) => T): T;
  getOrElse(arg: T | ((error: E) => T)): T {
    if (typeof arg === "function") {
      const onFailure = arg as (error: E) => T
      return this.isSuccess ? this.value! : onFailure(this.error!)
    }
    const defaultValue = arg as T
    return this.isSuccess ? this.value! : defaultValue
  }

  getOrThrow(): T {
    if (this.isSuccess) {
      return this.value!
    }
    throw this.error || new Error("Error in Result.getOrThrow")
  }
}

runCatchingを実装する

Kotlinでは全ての関数が同期的に実行されますが、javascriptでは非同期関数は await をしないと勝手に非同期で実行されてしまうので、 runCatching 及び runCatchingAsync を実装します。2種類になってしまうのがちょっとイマイチですね。 全ての関数を非同期関数でラップすれば統合できますが、必ず await runCatching() と書く必要があり負の連鎖が発生するため、統合はやめておきます。

import { Result } from "./result"

export const runCatching = <T, E = Error>(operation: () => T): Result<T, E> => {
  try {
    const value = operation()
    return Result.success<T, E>(value)
  } catch (error) {
    return Result.failure<T, E>(error as E)
  }
}
import { Result } from "./result"

export const runCatchingAsync = async <T, E = Error>(
  operation: () => Promise<T>
): Promise<Result<T, E>> => {
  try {
    const value = await operation()
    return Result.success<T, E>(value)
  } catch (error) {
    return Result.failure<T, E>(error as E)
  }
}

これで実装は完了です。

使ってみる

今まで try-catch を使っていた以下のような処理があるとします。

try {
  const hoge = calcHoge()
  setHoge(hoge)
} catch() {
  setHoge(undefined)
}

これが、このように書けるようになります。Reactの関数コンポーネントなどで便利に使えそうです。

const result = runCatching(calcHoge)
setHoge(result.getOrUndefined())

undefined を許容できない場合、 getOrElse を利用して初期値などのデータをセットすることもできます。

// 規定値を代入
const result = runCatching(calcHoge)
setHoge(result.getOrElse(0))

// fallback関数を実行
const result = runCatching(calcHoge)
setHoge(result.getOrElse(() => fallbackFn()))

また、runCatchingはブロックを作らないので「エラーをハンドリングしたいが結果をtry-catchのスコープ外に持っていきたい」というシチュエーションに便利です。

// letを使う場合
let result = 0
try {
  result = calc()
} catch() {}
nextTask(result)

// 関数化してletを使わないようにする場合
const result = (() => {
  try {
    return calc()
  } catch() {
    return 0
  } 
})
nextTask(result)

// runCatchingを使う場合
const result = runCatching(calc)
nextTask(result.getOrElse(0))

さいごに

こんな便利な関数が最初から存在するKotlinは羨ましいですね。Kotlinの全ての機能を理解して使いこなすのは大変ですが。

実は、typescriptで runCatching を実現したnpmは既に存在しています。ただ、インストール数が少なかったり、欲しい機能がなかったりで、今回は自分で実装することにしました(大した実装でもないので)。

頑張って探せば、同じような機能を実現しているnpmが世界のどこかにあるかもしれないですね。

この記事をシェア

弊社では、一緒に会社を面白くしてくれる仲間を募集しています。
お気軽にお問い合わせください!