TypeScriptでKotlinのrunCatchingを実装する
2023年 04月 28日 金曜日
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が世界のどこかにあるかもしれないですね。