🧑‍💻

【TypeScript】Result型を定義してエラーハンドリングを楽にする

2022/12/19に公開1

はじめに

TypeScriptでResult型を定義する方法はいくつかありますが、生成時や処理時の書き方が個人的に分かりにくいものだったので、初心者の方でも簡単に使えるResult型を作成したいと思います。

Result型とは?

Result型とは、何かしらの処理において、成功または失敗を表現できる型です。
これにより、try, catchでの苦悩を解放することができるようになります。

try, catchの問題点

TypeScriptでは、try, catchcatchした例外は、anyとして扱われます。
これでは、TypeScriptの型安全性の強みを全く活かすことができません。

try {
    doSomething();
} catch(e) {
    if (e instanceof HogeError) {}
    else if (e instanceof FugaError) {}
    .
    .
    .
    // eの型がanyであるため、絞ろうにも絞れない
}

完成コード

今回作成したResult型がこちらとなります。
工夫して点としては、Result.success()またはResult.failure()で生成できるようにしたことや、結果処理を簡単にできるようにwhen()を実装したことです。

class Result<T, E extends Error> {
  private readonly value: T | E;

  private constructor(value: T | E) {
    this.value = value;
  }

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

  static failure<E extends Error>(error: E): Result<any, E> {
    return new Result<any, E>(error);
  }

  when({
    success,
    failure,
  }: {
    success: (data: T) => any;
    failure: (error: E) => any;
  }) {
    if (this.value instanceof Error) {
      return failure(this.value);
    } else {
      return success(this.value);
    }
  }
}

使い方

使い方は簡単で、成功時のResultを生成したいときにはResult.success()、失敗時のResultを生成したいときはResult.failure()を使用するだけです。

Result.success(返したい値);
Result.failure(投げたいエラー);

また、返ってきたResult型に対して、成功時と失敗時の処理を実装する際には、when()を使用するだけです。

const result = something();
result.when({
  success(data) {
    成功時の処理
  },
  failure(error) {
    失敗時の処理
  },
});

使用例

class HogeError extends Error {}

class FugaError extends Error {}

// 戻り値について 
// 成功時:string 
// 失敗時:HogeErrorまたはFugaError
function foo(value: string): Result<string, HogeError | FugaError> {
  if (value === "foo") {
    return Result.success("success");
  } else {
    // throwの代わりにResult.failure()をreturnする
    return Result.failure(new HogeError());
  }
}
// 例1
const result1 = foo('foo');
result1.when({
  success(data) { // dataの型はstring
    // 実行される
    console.log(data); // foo
  },
  failure(error) { // errorの型はHogeErrorまたはFugaError
    // 実行されない
    if (error instanceof HogeError) {}
    else if (error instance of FugaError) {}
  },
});

// 例2
const result2 = foo('bar');
result2.when({
  success(data) { // dataの型はstring
    // 実行されない
  },
  failure(error) { // errorの型はHogeErrorまたはFugaError
    // 実行される
    if (error instanceof HogeError) {}
    else if (error instance of FugaError) {}
  },
});

Discussion

nap5nap5

neverthrowのResult型でチャレンジしてみました。

デモコードです。

https://codesandbox.io/p/sandbox/brave-microservice-3xjidh?file=%2Fsrc%2Findex3.ts

$ yarn do src/index3.ts

本記事でのwhen句にあたるのが、デモでのmatch句になります。

import { Chance } from "chance";
import { Err, Ok, Result } from "neverthrow";
import { z } from "zod";

interface CustomErrorData {}

export class TikError extends Error implements CustomErrorData {
  override readonly name = "TikError" as const;
  public readonly errorCode: string = "E01";
  constructor(message: string, options?: { cause: unknown }) {
    super(message, options);
    this.cause = options?.cause;
  }
}

export class TokError extends Error implements CustomErrorData {
  override readonly name = "TokError" as const;
  public readonly errorCode: string = "E02";
  constructor(message: string, options?: { cause: unknown }) {
    super(message, options);
    this.cause = options?.cause;
  }
}

const TikErrorDataSchema = z.custom<TikError>();
const TokErrorDataSchema = z.custom<TokError>();
export type TikErrorData = z.infer<typeof TikErrorDataSchema>;
export type TokErrorData = z.infer<typeof TokErrorDataSchema>;

async function foo(): Promise<Result<number, TikErrorData>> {
  return Chance().bool()
    ? new Ok(42)
    : new Err(new TikError("[foo]Something went wrong..."));
}

(async () => {
  const result = await foo();
  result.match(
    (t) => {
      console.log(t);
    },
    (e) => {
      console.log(e);
    }
  );
})();

簡単ですが、以上です。