Tasuke Hubのロゴ

ITを中心に困っている人を助けるメディア

分かりやすく解決策を提供することで、あなたの困ったをサポート。 全ての人々がスムーズに生活できる世界を目指します。

TypeScriptのエラーハンドリングガイド:初心者でも理解できる基本と実践例

記事のサムネイル
TH

Tasuke Hub管理人

東証プライム市場上場企業エンジニア

情報系修士卒業後、大手IT企業にてフルスタックエンジニアとして活躍。 Webアプリケーション開発からクラウドインフラ構築まで幅広い技術に精通し、 複数のプロジェクトでリードエンジニアを担当。 技術ブログやオープンソースへの貢献を通じて、日本のIT技術コミュニティに積極的に関わっている。

🎓情報系修士🏢東証プライム上場企業💻フルスタックエンジニア📝技術ブログ執筆者

TypeScriptのエラー処理とは?基本概念を理解しよう

TypeScriptのエラー処理とは、プログラム実行中に発生する可能性のある問題を適切に管理し、ユーザー体験を損なわずにシステムの安定性を保つための重要な技術です。初心者の方がつまずきやすいポイントですが、適切に理解すれば、より堅牢なアプリケーションが作れるようになります。

エラーとは何か?

プログラムにおけるエラーは主に以下の3種類に分類できます:

  1. 構文エラー(Syntax Errors): コードが言語の文法に従っていない場合に発生し、コンパイル時に検出されます
  2. 型エラー(Type Errors): TypeScriptの主要な機能である型チェックによって検出されるエラーです
  3. 実行時エラー(Runtime Errors): プログラム実行中に発生するエラーで、例外(Exception)とも呼ばれます

TypeScriptでは、1と2はコンパイル時に検出できるため、実行前に修正できますが、3の実行時エラーは適切にハンドリングする必要があります。

TypeScriptにおけるエラー処理の重要性

なぜエラー処理が特に重要なのでしょうか?

// エラー処理なしの例
function getUserData(userId: string) {
  const response = fetchUserFromAPI(userId); // ネットワークエラーが発生する可能性あり
  return response.data; // responseがnullやundefinedの場合、エラーになる
}

// エラー処理ありの例
function getUserData(userId: string): UserData | null {
  try {
    const response = fetchUserFromAPI(userId);
    return response.data;
  } catch (error) {
    console.error('ユーザーデータの取得に失敗しました:', error);
    return null; // エラー発生時は null を返す
  }
}

適切なエラー処理によって:

  • ユーザーに分かりやすいエラーメッセージを表示できます
  • アプリケーションが予期せず停止するのを防げます
  • デバッグが容易になり、問題の早期発見につながります
  • コードの信頼性と保守性が向上します

TypeScriptならではの型安全なエラー処理

TypeScriptの大きな利点は、型システムを活用したエラー処理ができることです。

// JavaScript風のエラー処理
function divide(a, b) {
  if (b === 0) {
    throw new Error("0で割ることはできません");
  }
  return a / b;
}

// TypeScriptの型を活用したエラー処理
type DivisionResult = {
  success: true;
  value: number;
} | {
  success: false;
  error: string;
}

function divideTS(a: number, b: number): DivisionResult {
  if (b === 0) {
    return { success: false, error: "0で割ることはできません" };
  }
  return { success: true, value: a / b };
}

// 使用例
const result = divideTS(10, 2);
if (result.success) {
  console.log(`結果: ${result.value}`);
} else {
  console.error(`エラー: ${result.error}`);
}

この例では、戻り値の型にエラー情報を含めることで、呼び出し側がエラーをチェックすることを強制しています。これはTypeScriptならではの型安全なアプローチです。

次のセクションでは、TypeScriptにおける標準的な例外処理の方法であるtry-catch構文について詳しく見ていきます。

このトピックはこちらの書籍で勉強するのがおすすめ!

この記事の内容をさらに深く理解したい方におすすめの一冊です。実践的な知識を身につけたい方は、ぜひチェックしてみてください!

TypeScriptにおける例外処理の基本:try-catch構文の使い方

TypeScriptでは、JavaScriptと同様にtry-catch-finally構文を使用して例外処理を行います。この構文はとてもシンプルですが、効果的に使うためにはいくつかのポイントを押さえておく必要があります。

基本的なtry-catch構文

try-catchの基本的な構文は以下のとおりです:

try {
  // エラーが発生する可能性のあるコード
} catch (error) {
  // エラーが発生した場合に実行されるコード
} finally {
  // エラーの有無にかかわらず必ず実行されるコード(省略可能)
}

簡単な例を見てみましょう:

function parseJSON(jsonString: string): unknown {
  try {
    return JSON.parse(jsonString);
  } catch (error) {
    console.error('JSONの解析に失敗しました:', error);
    return null;
  } finally {
    console.log('JSON解析処理が完了しました');
  }
}

// 使用例
const validJSON = '{"name": "田中", "age": 30}';
const invalidJSON = '{name: "田中", age: 30}'; // 無効なJSON(引用符がない)

console.log(parseJSON(validJSON)); // {"name": "田中", "age": 30}
console.log(parseJSON(invalidJSON)); // null

TypeScriptでのエラーオブジェクトの型付け

TypeScript 4.0より前のバージョンでは、catchブロックのerrorパラメータはデフォルトでany型でしたが、TypeScript 4.0以降ではunknown型になりました。これはより型安全ですが、エラーオブジェクトを使う前に型チェックが必要になります。

try {
  // エラーを発生させるコード
  throw new Error('テストエラー');
} catch (error) {
  // エラーがError型かどうかチェック
  if (error instanceof Error) {
    console.error(`エラーメッセージ: ${error.message}`);
    console.error(`スタックトレース: ${error.stack}`);
  } else {
    console.error('未知のエラー:', error);
  }
}

複数のエラータイプを扱う

さまざまな種類のエラーを異なる方法で処理したい場合は、instanceofを使って型チェックを行うことができます:

class DatabaseError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'DatabaseError';
  }
}

class ValidationError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'ValidationError';
  }
}

function processUserData(userId: string): void {
  try {
    // ユーザーの検証
    if (!isValidUserId(userId)) {
      throw new ValidationError('ユーザーIDが無効です');
    }
    
    // データベース操作
    const userData = fetchUserFromDatabase(userId);
    if (!userData) {
      throw new DatabaseError('ユーザーデータの取得に失敗しました');
    }
    
    // データ処理
    processData(userData);
    
  } catch (error) {
    if (error instanceof ValidationError) {
      // 検証エラーの処理
      console.error('検証エラー:', error.message);
      // ユーザーに入力を修正してもらうよう促す
    } else if (error instanceof DatabaseError) {
      // データベースエラーの処理
      console.error('データベースエラー:', error.message);
      // リトライまたは管理者に通知
    } else if (error instanceof Error) {
      // その他の一般的なエラー
      console.error('予期せぬエラー:', error.message);
    } else {
      // 未知のエラー
      console.error('未知のエラー:', error);
    }
  }
}

try-catchのスコープとパフォーマンス

try-catchブロックは、必要な部分だけを囲むようにしましょう。広すぎるスコープで囲むと、どこでエラーが発生したのか特定しづらくなります。

// 良くない例:スコープが広すぎる
try {
  validateInput(data);
  processData(data);
  saveToDatabase(data);
  notifyUser(data);
} catch (error) {
  // どのステップでエラーが発生したか分かりにくい
  console.error('処理中にエラーが発生しました:', error);
}

// 良い例:必要な部分だけを囲む
try {
  validateInput(data); // ここでエラーが発生する可能性が高い
} catch (error) {
  console.error('入力検証中にエラーが発生しました:', error);
  return;
}

try {
  saveToDatabase(data); // ここでエラーが発生する可能性が高い
} catch (error) {
  console.error('データベース保存中にエラーが発生しました:', error);
  return;
}

// エラーが発生しにくい処理はtry-catchで囲まない
processData(data);
notifyUser(data);

また、try-catchはパフォーマンスにわずかな影響を与えることがあります。特にホットパス(頻繁に実行されるコードパス)では、必要な場合にのみ使用することを検討してください。

アンチパターンと注意点

  1. エラーを握りつぶさない: catchブロック内でエラーを処理せずに無視するのは避けましょう
// 悪い例
try {
  riskyOperation();
} catch (error) {
  // 何もしない(エラーを握りつぶす)
}

// 良い例
try {
  riskyOperation();
} catch (error) {
  console.error('エラーが発生しました:', error);
  // 適切に対処するか、必要に応じて上位に再スローする
  throw error;
}
  1. 過剰な再スロー: 単にエラーをキャッチしてすぐに再スローするだけならtry-catchを使う意味がありません
// 意味のないパターン
try {
  riskyOperation();
} catch (error) {
  throw error; // 何も処理していない
}

// 有益なパターン
try {
  riskyOperation();
} catch (error) {
  // エラーを拡張または変換
  throw new EnhancedError('操作に失敗しました', { cause: error });
}

TypeScriptでのtry-catch構文は、以上のようにさまざまな場面で効果的に使えます。次のセクションでは、より高度なエラー処理のためのカスタムエラークラスとエラー型の定義について見ていきましょう。

あわせて読みたい

このトピックはこちらの書籍で勉強するのがおすすめ!

この記事の内容をさらに深く理解したい方におすすめの一冊です。実践的な知識を身につけたい方は、ぜひチェックしてみてください!

カスタムエラークラスの作成とエラー型の定義

TypeScriptでエラー処理をより強力にするには、カスタムエラークラスの作成とエラー型の定義が効果的です。これにより、型安全性が向上し、特定のエラー状況に対して適切に対応できるようになります。

基本的なカスタムエラークラスの作成

TypeScriptでは、組み込みのErrorクラスを拡張して独自のエラークラスを作成できます:

class AppError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'AppError';
    
    // これはTypeScriptで継承したエラーのプロトタイプチェーンを修正するために必要
    Object.setPrototypeOf(this, AppError.prototype);
  }
}

// 使用例
try {
  throw new AppError('アプリケーション固有のエラーが発生しました');
} catch (error) {
  if (error instanceof AppError) {
    console.error('AppError:', error.message);
  }
}

エラー階層の構築

より詳細なエラー処理を行うために、エラー階層を構築することもできます:

// ベースエラークラス
class AppError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'AppError';
    Object.setPrototypeOf(this, AppError.prototype);
  }
}

// 特定の機能に関するエラー
class APIError extends AppError {
  constructor(
    message: string,
    public statusCode: number
  ) {
    super(message);
    this.name = 'APIError';
    Object.setPrototypeOf(this, APIError.prototype);
  }
}

// さらに特化したエラー
class NotFoundError extends APIError {
  constructor(resource: string) {
    super(`リソースが見つかりません: ${resource}`, 404);
    this.name = 'NotFoundError';
    Object.setPrototypeOf(this, NotFoundError.prototype);
  }
}

class ValidationError extends APIError {
  constructor(
    message: string,
    public details: Record<string, string>
  ) {
    super(message, 400);
    this.name = 'ValidationError';
    Object.setPrototypeOf(this, ValidationError.prototype);
  }
}

// 使用例
function getUserData(userId: string) {
  if (!userId) {
    throw new ValidationError('ユーザーIDは必須です', { userId: '空であってはなりません' });
  }
  
  const user = findUserById(userId);
  if (!user) {
    throw new NotFoundError(`User(${userId})`);
  }
  
  return user;
}

// エラーハンドリング
try {
  const user = getUserData('');
} catch (error) {
  if (error instanceof ValidationError) {
    console.error('バリデーションエラー:', error.message);
    console.error('詳細:', error.details);
  } else if (error instanceof NotFoundError) {
    console.error('リソースが見つかりません:', error.message);
  } else if (error instanceof APIError) {
    console.error(`APIエラー (${error.statusCode}):`, error.message);
  } else {
    console.error('未知のエラー:', error);
  }
}

エラーコードとエラーメッセージの分離

エラーメッセージをコードから分離し、多言語対応やメッセージの一貫性を保つこともできます:

// エラーコードの定義
enum ErrorCode {
  VALIDATION_ERROR = 'VALIDATION_ERROR',
  NOT_FOUND = 'NOT_FOUND',
  UNAUTHORIZED = 'UNAUTHORIZED',
  INTERNAL_ERROR = 'INTERNAL_ERROR',
}

// エラーメッセージのマッピング
const errorMessages: Record<ErrorCode, string> = {
  [ErrorCode.VALIDATION_ERROR]: '入力データが無効です',
  [ErrorCode.NOT_FOUND]: 'リソースが見つかりません',
  [ErrorCode.UNAUTHORIZED]: '認証に失敗しました',
  [ErrorCode.INTERNAL_ERROR]: 'サーバー内部エラーが発生しました',
};

// コードを含むエラークラス
class CodedError extends Error {
  constructor(
    public readonly code: ErrorCode,
    public readonly details?: unknown
  ) {
    super(errorMessages[code]);
    this.name = 'CodedError';
    Object.setPrototypeOf(this, CodedError.prototype);
  }
}

// 使用例
function validateUser(userData: unknown): asserts userData is UserData {
  if (!userData || typeof userData !== 'object') {
    throw new CodedError(ErrorCode.VALIDATION_ERROR, { message: 'ユーザーデータはオブジェクトである必要があります' });
  }
  
  // 他のバリデーションロジック...
}

try {
  validateUser(null);
} catch (error) {
  if (error instanceof CodedError && error.code === ErrorCode.VALIDATION_ERROR) {
    console.error('バリデーションエラー:', error.message);
    console.error('詳細:', error.details);
  }
}

型レベルでのエラー表現:Result型

TypeScriptでは、エラーを表現するためのより型安全な方法として、Result型を使うこともできます:

// 成功または失敗を表現するユニオン型
type Result<T, E = Error> = Success<T> | Failure<E>;

// 成功結果
interface Success<T> {
  readonly success: true;
  readonly value: T;
}

// 失敗結果
interface Failure<E> {
  readonly success: false;
  readonly error: E;
}

// Result型を作成するヘルパー関数
function success<T>(value: T): Success<T> {
  return { success: true, value };
}

function failure<E>(error: E): Failure<E> {
  return { success: false, error };
}

// ドメイン固有のエラー型
type UserError = 
  | { type: 'NOT_FOUND'; id: string }
  | { type: 'INVALID_INPUT'; fields: string[] }
  | { type: 'UNAUTHORIZED' };

// Result型を使った関数
function findUser(id: string): Result<User, UserError> {
  if (!id) {
    return failure({ type: 'INVALID_INPUT', fields: ['id'] });
  }
  
  const user = getUserFromDatabase(id);
  if (!user) {
    return failure({ type: 'NOT_FOUND', id });
  }
  
  return success(user);
}

// 使用例
const userResult = findUser('user123');

if (userResult.success) {
  // 型推論により、userResult.value は User 型
  console.log('ユーザーが見つかりました:', userResult.value.name);
} else {
  // 型推論により、userResult.error は UserError 型
  switch (userResult.error.type) {
    case 'NOT_FOUND':
      console.error(`ID ${userResult.error.id} のユーザーが見つかりません`);
      break;
    case 'INVALID_INPUT':
      console.error(`無効なフィールド: ${userResult.error.fields.join(', ')}`);
      break;
    case 'UNAUTHORIZED':
      console.error('権限がありません');
      break;
  }
}

この方法では、例外を投げる代わりに明示的にエラーを返すことで、コンパイラーが呼び出し側にエラー処理を強制します。これは特に副作用を避けたい関数型プログラミングのアプローチと相性が良いです。

TypeScriptのインターセクション型を使ったエラー強化

特定のエラーに追加情報を付与したい場合は、インターセクション型が便利です:

// 基本エラーインターフェース
interface BaseError {
  message: string;
  name: string;
}

// タイムスタンプ情報をエラーに追加
interface TimestampedError {
  timestamp: Date;
}

// HTTPレスポンス情報をエラーに追加
interface HTTPError {
  statusCode: number;
  url: string;
}

// 複合エラータイプ
type APIErrorWithTimestamp = BaseError & HTTPError & TimestampedError;

// エラーを作成する関数
function createAPIError(message: string, statusCode: number, url: string): APIErrorWithTimestamp {
  return {
    message,
    name: 'APIError',
    statusCode,
    url,
    timestamp: new Date(),
  };
}

// 使用例
const error = createAPIError('リソースが見つかりません', 404, 'https://api.example.com/users/123');
console.error(
  `${error.name}: ${error.message}\n` +
  `URL: ${error.url}\n` +
  `Status: ${error.statusCode}\n` +
  `Time: ${error.timestamp.toISOString()}`
);

TypeScriptのカスタムエラークラスとエラー型を活用することで、例外処理をより型安全かつ構造化された方法で実装できます。次のセクションでは、非同期処理におけるエラーハンドリングについて見ていきましょう。

このトピックはこちらの書籍で勉強するのがおすすめ!

この記事の内容をさらに深く理解したい方におすすめの一冊です。実践的な知識を身につけたい方は、ぜひチェックしてみてください!

非同期処理のエラーハンドリング:Promise、async/await

TypeScriptでは、非同期処理におけるエラーハンドリングも重要なトピックです。Promise、async/awaitを使った非同期コードでのエラー処理方法を見ていきましょう。

Promiseのエラーハンドリング

Promiseを使用する場合、.catch()メソッドを使ってエラーをキャッチします:

function fetchUserData(userId: string): Promise<UserData> {
  return fetch(`https://api.example.com/users/${userId}`)
    .then(response => {
      if (!response.ok) {
        throw new Error(`APIエラー: ${response.status}`);
      }
      return response.json();
    })
    .then(data => {
      // データの検証
      if (!data.name) {
        throw new Error('ユーザーデータが不完全です');
      }
      return data as UserData;
    })
    .catch(error => {
      console.error('ユーザーデータの取得に失敗しました:', error);
      throw error; // 上位に再スロー
    });
}

// 使用例
fetchUserData('123')
  .then(userData => {
    console.log('ユーザー名:', userData.name);
  })
  .catch(error => {
    console.error('エラーが発生しました:', error.message);
  });

Promiseチェーンでのエラー伝播

Promiseチェーンでは、どこかの段階で発生したエラーは次の.catch()までスキップされます:

fetchUserData('123')
  .then(userData => processUserData(userData))
  .then(result => saveToDatabase(result))
  .then(saveResult => {
    console.log('保存完了:', saveResult);
    return notifyUser(saveResult);
  })
  .catch(error => {
    // 上記のどのステップでエラーが発生しても、ここでキャッチされる
    console.error('処理中にエラーが発生しました:', error);
  })
  .finally(() => {
    // エラーの有無にかかわらず実行される
    console.log('処理が完了しました');
  });

async/awaitを使ったエラーハンドリング

async/awaitを使う場合は、通常のtry-catchブロックでエラーをキャッチします:

async function getUserDataAndProcess(userId: string): Promise<void> {
  try {
    const userData = await fetchUserData(userId);
    const processedData = await processUserData(userData);
    const saveResult = await saveToDatabase(processedData);
    console.log('保存完了:', saveResult);
    await notifyUser(saveResult);
  } catch (error) {
    // 非同期処理のどこでエラーが発生しても、ここでキャッチされる
    console.error('処理中にエラーが発生しました:', error);
    // エラー情報に基づいて適切な処理を行う
    if (error instanceof ApiError) {
      // API関連のエラー処理
    } else if (error instanceof DatabaseError) {
      // データベース関連のエラー処理
    }
  } finally {
    // クリーンアップ処理など
    console.log('処理が完了しました');
  }
}

async/awaitを使うと、同期コードのようにエラー処理を書けるため、可読性が高まります。

非同期関数の戻り値の型付け

非同期関数の戻り値には、より具体的な型を指定することをお勧めします:

// あまり良くない例
async function fetchUser(id: string): Promise<any> {
  // ...
}

// 良い例
interface User {
  id: string;
  name: string;
  email: string;
}

async function fetchUser(id: string): Promise<User> {
  // ...
}

エラーが発生する可能性がある場合は、以下のような方法もあります:

// nullを返すパターン
async function fetchUser(id: string): Promise<User | null> {
  try {
    // ...
    return user;
  } catch (error) {
    console.error('ユーザー取得エラー:', error);
    return null;
  }
}

// Result型を使うパターン
type AsyncResult<T> = Promise<Result<T, Error>>;

async function fetchUser(id: string): AsyncResult<User> {
  try {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) {
      return failure(new Error(`APIエラー: ${response.status}`));
    }
    const user = await response.json();
    return success(user);
  } catch (error) {
    return failure(error instanceof Error ? error : new Error(String(error)));
  }
}

// 使用例
const result = await fetchUser('123');
if (result.success) {
  console.log('ユーザー:', result.value);
} else {
  console.error('エラー:', result.error);
}

複数の非同期処理の並行実行とエラーハンドリング

複数の非同期処理を並行して実行する場合のエラーハンドリングも重要です:

Promise.all

すべてのPromiseが成功した場合にのみ成功し、一つでも失敗すると即座に失敗します:

try {
  const results = await Promise.all([
    fetchUser('123'),
    fetchPosts('123'),
    fetchComments('123')
  ]);
  
  const [user, posts, comments] = results;
  // 全てのデータを使った処理
} catch (error) {
  // いずれかのPromiseが失敗した場合に実行される
  console.error('いずれかのデータ取得に失敗しました:', error);
}

Promise.allSettled

各Promiseの結果に関わらず、すべての処理の完了を待ちます:

const results = await Promise.allSettled([
  fetchUser('123'),
  fetchPosts('123'),
  fetchComments('123')
]);

// 各結果をチェック
results.forEach((result, index) => {
  if (result.status === 'fulfilled') {
    console.log(`処理${index + 1}が成功:`, result.value);
  } else {
    console.error(`処理${index + 1}が失敗:`, result.reason);
  }
});

// 成功した結果だけを抽出
const successfulResults = results
  .filter((result): result is PromiseFulfilledResult<unknown> => result.status === 'fulfilled')
  .map(result => result.value);

Promise.any

最初に成功したPromiseの結果を返し、すべてが失敗した場合のみ失敗します:

try {
  // 複数のAPIから同じデータを取得し、最初に成功したものを使用
  const userData = await Promise.any([
    fetchUserFromAPI1(userId),
    fetchUserFromAPI2(userId),
    fetchUserFromAPI3(userId)
  ]);
  
  console.log('ユーザーデータを取得しました:', userData);
} catch (error) {
  // AggregateErrorが投げられる(すべてのPromiseが失敗した場合)
  if (error instanceof AggregateError) {
    console.error('すべてのAPIからデータ取得に失敗しました');
    console.error('エラー一覧:', error.errors);
  }
}

非同期の例外ハンドリングのベストプラクティス

  1. 非同期関数は常にPromiseを返す: async関数内のすべてのパスがPromiseを返すようにします
// 良くない例
async function fetchData(id: string) {
  if (!id) {
    return null; // Promiseでラップされる
  }
  
  // 非同期処理
  const data = await apiCall(id);
  return data;
}

// 良い例
async function fetchData(id: string): Promise<Data | null> {
  if (!id) {
    return Promise.resolve(null); // 明示的にPromiseを返す
  }
  
  const data = await apiCall(id);
  return data;
}
  1. awaitしていないPromiseに注意: awaitしないとエラーがキャッチされない場合があります
// 良くない例(エラーがキャッチされない)
async function processData() {
  try {
    apiCall(); // awaitしていないので、この関数内でエラーがキャッチされない
  } catch (error) {
    console.error('エラー:', error); // 実行されない
  }
}

// 良い例
async function processData() {
  try {
    await apiCall(); // ここでawaitすることでエラーがキャッチされる
  } catch (error) {
    console.error('エラー:', error);
  }
}
  1. 非同期処理のエラー境界を適切に設定: すべてのエラーを一カ所でキャッチするのではなく、適切な境界を設定します
// コンポーネント内のエラー境界の例(React風)
async function UserProfileComponent() {
  try {
    // ユーザー情報の取得
    const user = await fetchUser(userId);
    
    try {
      // ユーザーの投稿履歴の取得(別のエラー境界)
      const posts = await fetchUserPosts(userId);
      renderPosts(posts);
    } catch (error) {
      // 投稿情報の取得に失敗しても、ユーザー情報自体は表示できる
      console.error('投稿の取得に失敗しました:', error);
      renderPostError();
    }
    
    renderUserProfile(user);
  } catch (error) {
    // ユーザー情報の取得に失敗した場合のフォールバック
    console.error('ユーザー情報の取得に失敗しました:', error);
    renderUserError();
  }
}

TypeScriptでの非同期処理のエラーハンドリングを適切に行うことで、より堅牢でメンテナンスしやすいコードが書けるようになります。次のセクションでは、より実践的なエラー設計パターンについて見ていきましょう。

このトピックはこちらの書籍で勉強するのがおすすめ!

この記事の内容をさらに深く理解したい方におすすめの一冊です。実践的な知識を身につけたい方は、ぜひチェックしてみてください!

関連記事

実践的なエラー設計パターン:Result型とOption型

ここでは、TypeScriptでのより実践的なエラー設計パターンについて見ていきます。特に関数型プログラミングの影響を受けたResult型とOption型は、型安全なエラーハンドリングを実現するための強力なパターンです。

Result型の深堀り

前述のResult型をさらに発展させて、より実用的な実装を見ていきましょう:

// Result型の基本的な定義
type Result<T, E> = Success<T> | Failure<E>;

interface Success<T> {
  readonly _tag: 'success';
  readonly value: T;
}

interface Failure<E> {
  readonly _tag: 'failure';
  readonly error: E;
}

// ファクトリ関数
const success = <T>(value: T): Success<T> => ({
  _tag: 'success',
  value,
});

const failure = <E>(error: E): Failure<E> => ({
  _tag: 'failure',
  error,
});

// Result型の便利なユーティリティ関数
const isSuccess = <T, E>(result: Result<T, E>): result is Success<T> => 
  result._tag === 'success';

const isFailure = <T, E>(result: Result<T, E>): result is Failure<E> => 
  result._tag === 'failure';

// 結果を変換する関数
const map = <T, U, E>(result: Result<T, E>, fn: (value: T) => U): Result<U, E> =>
  isSuccess(result) ? success(fn(result.value)) : result;

// エラーを変換する関数
const mapError = <T, E, F>(result: Result<T, E>, fn: (error: E) => F): Result<T, F> =>
  isFailure(result) ? failure(fn(result.error)) : result;

// チェーン処理のための関数
const flatMap = <T, U, E>(result: Result<T, E>, fn: (value: T) => Result<U, E>): Result<U, E> =>
  isSuccess(result) ? fn(result.value) : result;

// 結果を取得するか、デフォルト値を返す
const getOrElse = <T, E>(result: Result<T, E>, defaultValue: T): T =>
  isSuccess(result) ? result.value : defaultValue;

// エラーをスローする
const getOrThrow = <T, E>(result: Result<T, E>): T => {
  if (isSuccess(result)) {
    return result.value;
  }
  throw result.error instanceof Error
    ? result.error
    : new Error(String(result.error));
};

これらのユーティリティ関数を使った実際の例を見てみましょう:

interface User {
  id: string;
  name: string;
  email: string;
}

// 様々なエラー型
type ApiError = 
  | { kind: 'network'; message: string }
  | { kind: 'not_found'; id: string }
  | { kind: 'auth'; message: string };

// APIから非同期でユーザーを取得する関数
async function fetchUser(id: string): Promise<Result<User, ApiError>> {
  try {
    const response = await fetch(`/api/users/${id}`);
    
    if (!response.ok) {
      if (response.status === 404) {
        return failure({ kind: 'not_found', id });
      }
      if (response.status === 401 || response.status === 403) {
        return failure({ kind: 'auth', message: '認証に失敗しました' });
      }
      return failure({ kind: 'network', message: `APIエラー: ${response.status}` });
    }
    
    const data = await response.json();
    return success(data as User);
  } catch (error) {
    return failure({ kind: 'network', message: error instanceof Error ? error.message : String(error) });
  }
}

// 使用例
async function displayUserProfile(userId: string): Promise<void> {
  const userResult = await fetchUser(userId);
  
  // パターンマッチング風のエラーハンドリング
  if (isFailure(userResult)) {
    const error = userResult.error;
    switch (error.kind) {
      case 'not_found':
        console.error(`ユーザーID ${error.id} は見つかりませんでした`);
        showNotFoundMessage();
        break;
      case 'auth':
        console.error(`認証エラー: ${error.message}`);
        redirectToLogin();
        break;
      case 'network':
        console.error(`ネットワークエラー: ${error.message}`);
        showOfflineMessage();
        break;
    }
    return;
  }
  
  // 成功した場合の処理
  const user = userResult.value;
  renderUserProfile(user);
  
  // ユーティリティ関数を使った例
  const greetingResult = map(userResult, user => `こんにちは、${user.name}さん!`);
  if (isSuccess(greetingResult)) {
    displayGreeting(greetingResult.value);
  }
}

Option型によるnullとundefinedの取り扱い

nullやundefinedによるエラーを型安全に扱うためのOption型も便利です:

// Option型の定義
type Option<T> = Some<T> | None;

interface Some<T> {
  readonly _tag: 'some';
  readonly value: T;
}

interface None {
  readonly _tag: 'none';
}

// ファクトリ関数
const some = <T>(value: T): Some<T> => ({
  _tag: 'some',
  value,
});

const none: None = { _tag: 'none' };

// オプション値を作成するヘルパー関数
const fromNullable = <T>(value: T | null | undefined): Option<T> =>
  value !== null && value !== undefined ? some(value) : none;

// 型ガード
const isSome = <T>(option: Option<T>): option is Some<T> => 
  option._tag === 'some';

const isNone = <T>(option: Option<T>): option is None => 
  option._tag === 'none';

// ユーティリティ関数
const map = <T, U>(option: Option<T>, fn: (value: T) => U): Option<U> =>
  isSome(option) ? some(fn(option.value)) : none;

const flatMap = <T, U>(option: Option<T>, fn: (value: T) => Option<U>): Option<U> =>
  isSome(option) ? fn(option.value) : none;

const getOrElse = <T>(option: Option<T>, defaultValue: T): T =>
  isSome(option) ? option.value : defaultValue;

const getOrUndefined = <T>(option: Option<T>): T | undefined =>
  isSome(option) ? option.value : undefined;

Option型を使った例:

// オブジェクトからプロパティを安全に取得する関数
function getProperty<T, K extends keyof T>(obj: T, key: K): Option<T[K]> {
  return fromNullable(obj[key]);
}

// ネストされたオブジェクトからプロパティを安全に取得
function getNestedProperty<T>(
  obj: T | null | undefined,
  ...keys: string[]
): Option<unknown> {
  let current: any = obj;
  
  for (const key of keys) {
    if (current == null || typeof current !== 'object') {
      return none;
    }
    current = current[key];
  }
  
  return fromNullable(current);
}

// 使用例
const user = {
  name: 'スズキ',
  address: {
    city: '東京',
    postalCode: '123-4567'
  },
  // contactはundefined
};

// 安全なプロパティアクセス
const nameOption = getProperty(user, 'name');
if (isSome(nameOption)) {
  console.log(`名前: ${nameOption.value}`);
}

// 存在しないプロパティへのアクセス
const contactOption = getProperty(user, 'contact');
console.log(`連絡先: ${getOrElse(contactOption, '未設定')}`);

// ネストされたプロパティへのアクセス
const cityOption = getNestedProperty(user, 'address', 'city');
const districtOption = getNestedProperty(user, 'address', 'district');

console.log(`都市: ${getOrElse(cityOption as Option<string>, '不明')}`);
console.log(`地区: ${getOrElse(districtOption as Option<string>, '不明')}`);

// オプション値をマップする例
const postalCodeOption = getNestedProperty(user, 'address', 'postalCode');
const formattedPostalCode = map(postalCodeOption as Option<string>, code => 
  `〒${code}`
);

console.log(`郵便番号: ${getOrElse(formattedPostalCode, '不明')}`);

Result型とOption型を組み合わせる

Result型とOption型を組み合わせることで、より表現力豊かなエラーハンドリングが可能になります:

// Option型からResult型への変換
function fromOption<T, E>(option: Option<T>, error: E): Result<T, E> {
  return isSome(option) ? success(option.value) : failure(error);
}

// 使用例
function findUserById(id: string): Option<User> {
  // ユーザーを検索する処理
  const user = userDatabase.find(u => u.id === id);
  return fromNullable(user);
}

function getUserOrFail(id: string): Result<User, Error> {
  return fromOption(
    findUserById(id),
    new Error(`ユーザーID ${id} は見つかりませんでした`)
  );
}

// チェーンでの活用例
async function processUserData(userId: string): Promise<Result<ProcessedData, Error>> {
  return getUserOrFail(userId)
    .flatMap(user => validateUser(user))
    .flatMap(validUser => processUser(validUser));
}

パイプラインパターンの実装

関数型プログラミングのパイプラインパターンを実装すると、エラーハンドリングをさらに直感的に書けるようになります:

// パイプライン演算子
function pipe<T, U>(value: T, fn: (value: T) => U): U {
  return fn(value);
}

// Resultに特化したパイプライン
function pipeResult<T, E, U>(
  result: Result<T, E>,
  fn: (value: T) => Result<U, E>
): Result<U, E> {
  return flatMap(result, fn);
}

// 使用例
async function createUser(userData: unknown): Promise<Result<User, Error>> {
  return pipe(
    validateUserData(userData),
    result => pipeResult(result, validData => checkDuplicateEmail(validData)),
    result => pipeResult(result, checkedData => saveUserToDatabase(checkedData))
  );
}

実際のアプリケーションでの活用例

実際のアプリケーションでResult型とOption型を活用する例を見てみましょう:

// ユーザー登録処理(実践的な例)
interface RegistrationData {
  username: string;
  email: string;
  password: string;
}

type ValidationError = {
  field: string;
  message: string;
}

type RegistrationError = 
  | { type: 'validation'; errors: ValidationError[] }
  | { type: 'duplicate'; field: string; value: string }
  | { type: 'server'; message: string };

// バリデーション関数
function validateRegistrationData(data: unknown): Result<RegistrationData, ValidationError[]> {
  const errors: ValidationError[] = [];
  
  // 型チェック
  if (!data || typeof data !== 'object') {
    return failure([{ field: 'data', message: '無効な入力データ' }]);
  }
  
  const { username, email, password } = data as any;
  
  // 各フィールドのバリデーション
  if (!username || typeof username !== 'string') {
    errors.push({ field: 'username', message: 'ユーザー名は必須です' });
  } else if (username.length < 3) {
    errors.push({ field: 'username', message: 'ユーザー名は3文字以上必要です' });
  }
  
  if (!email || typeof email !== 'string') {
    errors.push({ field: 'email', message: 'メールアドレスは必須です' });
  } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
    errors.push({ field: 'email', message: '有効なメールアドレスを入力してください' });
  }
  
  if (!password || typeof password !== 'string') {
    errors.push({ field: 'password', message: 'パスワードは必須です' });
  } else if (password.length < 8) {
    errors.push({ field: 'password', message: 'パスワードは8文字以上必要です' });
  }
  
  return errors.length > 0
    ? failure(errors)
    : success({ username, email, password });
}

// ユーザー登録処理
async function registerUser(data: unknown): Promise<Result<User, RegistrationError>> {
  // バリデーション
  const validatedResult = validateRegistrationData(data);
  if (isFailure(validatedResult)) {
    return failure({ type: 'validation', errors: validatedResult.error });
  }
  
  const registrationData = validatedResult.value;
  
  try {
    // 重複チェック
    const existingUser = await findUserByEmail(registrationData.email);
    if (isSome(existingUser)) {
      return failure({
        type: 'duplicate',
        field: 'email',
        value: registrationData.email
      });
    }
    
    // ユーザー作成
    const hashedPassword = await hashPassword(registrationData.password);
    const newUser = await createUserInDatabase({
      ...registrationData,
      password: hashedPassword
    });
    
    return success(newUser);
  } catch (error) {
    return failure({
      type: 'server',
      message: error instanceof Error ? error.message : '不明なエラー'
    });
  }
}

// クライアントでの使用例
async function handleRegistration(formData: FormData) {
  const result = await registerUser(Object.fromEntries(formData));
  
  if (isFailure(result)) {
    const error = result.error;
    
    switch (error.type) {
      case 'validation':
        displayValidationErrors(error.errors);
        break;
      case 'duplicate':
        displayDuplicateError(`この${error.field}は既に使用されています: ${error.value}`);
        break;
      case 'server':
        displayServerError(`サーバーエラー: ${error.message}`);
        break;
    }
    return;
  }
  
  // 登録成功
  const user = result.value;
  redirectToWelcomePage(user);
}

これらのパターンを適切に活用することで、TypeScriptの型システムのパワーを最大限に引き出し、より安全で保守性の高いコードを書くことができます。次のセクションでは、TypeScriptでのエラーハンドリングのベストプラクティスについてまとめていきます。

このトピックはこちらの書籍で勉強するのがおすすめ!

この記事の内容をさらに深く理解したい方におすすめの一冊です。実践的な知識を身につけたい方は、ぜひチェックしてみてください!

TypeScriptのエラーハンドリングベストプラクティス

このトピックはこちらの書籍で勉強するのがおすすめ!

この記事の内容をさらに深く理解したい方におすすめの一冊です。実践的な知識を身につけたい方は、ぜひチェックしてみてください!

おすすめ記事

おすすめコンテンツ