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が世界のどこかにあるかもしれないですね。