SODA Engineering Blog
💭

Result型使ってますか?

2024/12/06に公開

\スニダンを開発しているSODA inc.の Advent Calendar 2024 6日目の記事です!!!/

こんにちは、昨日と同じく私Mapleの記事です。
「Result型使ってますか?」解説していきます!


TypeScriptでのエラーハンドリングは、従来のtry-catch構文を用いる方法が一般的です。しかし、関数の戻り値としてエラーを扱うResult型を使用することで、より安全で明確なコードを書くことができます。
本記事では、Result型のメリットや、try-catchと比較した際の優位性について詳しく解説します。

1. Result型とは

Result型は、操作が成功したか失敗したかを明示的に表す型です。
以下の2つの状態を持ちます。

  • 成功(Ok):期待された結果が得られた場合。
  • 失敗(Err):エラー情報を含む場合。

Result型の定義例

type Result<T, E> = Ok<T> | Err<E>;

interface Ok<T> {
  isOk: true;
  value: T;
}

interface Err<E> {
  isOk: false;
  error: E;
}

2. Result型のメリット

2.1 明示的なエラーハンドリング

関数の戻り値にエラーが含まれる可能性を明示的に示すことができます。

function parseJSON<T>(input: string): Result<T, Error> {
  try {
    const parsed = JSON.parse(input);
    return { isOk: true, value: parsed };
  } catch (e) {
    return { isOk: false, error: e };
  }
}

2.2 型安全性の向上

Result型を用いると、コンパイラがエラーハンドリングの漏れを検出できます。これは、開発者がエラー処理を忘れるリスクを減少させます。

const result = parseJSON<{ name: string }>('{"name": "Alice"}');

if (result.isOk) {
  console.log(result.value.name);
} else {
  console.error('パースに失敗しました:', result.error);
}

2.3 非同期処理との相性の良さ

非同期処理でのエラーハンドリングも、Result型を使うことで一貫性を保てます。

async function fetchData(url: string): Promise<Result<Data, Error>> {
  try {
    const response = await fetch(url);
    const data = await response.json();
    return { isOk: true, value: data };
  } catch (e) {
    return { isOk: false, error: e };
  }
}

2.4 例外の予測可能性

try-catchでは、関数内部で予期せぬ例外が発生する可能性がありますが、Result型を使用すると、エラーが戻り値として扱われるため、例外の伝播を制御できます。

3. try-catchとの比較

3.1 try-catchの課題

  • 暗黙的なエラー処理:エラーが発生するかどうかが分かりにくい。
  • 非同期処理での複雑性async/awaitと組み合わせると、try-catchがネストしがち。
function parseJSON<T>(input: string): T {
  return JSON.parse(input);
}

try {
  const data = parseJSON<{ name: string }>('{"name": "Alice"}');
  console.log(data.name);
} catch (e) {
  console.error('パースに失敗しました:', e);
}

3.2 Result型の優位性

  • 明示的なエラー情報:関数の戻り値でエラーを扱うため、エラー処理が明確。
  • コンポーザビリティ:エラー処理を一元管理できる。
  • テストの容易性:テストがしやすい。

4. Result型を用いた関数の組み合わせ

Result型を使うと、複数の関数を組み合わせる際のエラーハンドリングがシンプルになります。

function validate(data: any): Result<ValidatedData, ValidationError> {
  // バリデーション処理
}

function save(data: ValidatedData): Result<SavedData, SaveError> {
  // データ保存処理
}

const result = parseJSON(rawInput)
  .then(validate)
  .then(save);

if (result.isOk) {
  console.log('データ保存に成功しました:', result.value);
} else {
  console.error('エラーが発生しました:', result.error);
}

5. エラーハンドリングの例

5.1 パターンマッチング風の処理

TypeScriptの型ガードを用いて、Result型の状態に応じた処理ができます。

function isOk<T, E>(result: Result<T, E>): result is Ok<T> {
  return result.isOk;
}

if (isOk(result)) {
  // 成功時の処理
} else {
  // エラー時の処理
}

5.2 unwrap関数の使用

安全にResult型の値を取り出す関数を作成。

function unwrap<T, E>(result: Result<T, E>): T {
  if (result.isOk) {
    return result.value;
  } else {
    throw result.error;
  }
}

try {
  const data = unwrap(result);
  console.log(data);
} catch (e) {
  console.error('エラーが発生しました:', e);
}

6. まとめ

  • Result型のメリット

    • エラー処理が明示的になり、コードの可読性と安全性が向上。
    • 型システムを活用して、エラーハンドリングの漏れを防止。
    • 関数の合成や非同期処理との組み合わせが容易。
  • try-catchのデメリット

    • エラーが関数シグネチャに現れず、暗黙的なエラー処理となる。
    • 非同期処理でのネストが深くなり、コードが複雑化。
    • 型安全性が低く、エラーハンドリングの漏れをコンパイラが検出できない。

**Result型を用いることで、メンテナンス性の高いコードを書くことが可能だと考えています!
**TypeScriptの型システムを最大限に活用し、エラーハンドリングを明示的かつ効率的に行いましょう!

SODA Engineering Blog
SODA Engineering Blog

Discussion