TypeScript / LESSON 16 / TS16

TypeScript:エラー処理と型

1学習の目的

2基礎解説

JS10では例外(throw / catch)を学びました。TypeScriptにはもう1つの選択肢があります——失敗を「例外」ではなく「戻り値」として扱う方法です。

方法書き方向いている場面
例外throw new Error(…)想定外の異常
カスタムエラーclass XError extends Errorエラーの種類を分けたい
Result 型{ ok: true, value } | { ok: false, error }想定内の失敗
網羅性チェックconst _: never = value分岐漏れの検出
✅ 覚えるべき重要ポイント
  1. 例外は型に現れないfunction f(): number と書いてあっても、実は throw するかもしれない——呼び出す側は型からそれを知れない
  2. Result 型なら失敗が型に現れる。戻り値が { ok: false } の可能性を含むので、呼び出し側は必ず確認を迫られる
  3. Error を継承したカスタムエラーを作ると、instanceof で種類を判別できる。追加の情報(どの項目でエラーか等)も持たせられる。
  4. エラーの種類もタグ付きユニオンで表せる(TS09)。{ kind: "network" } | { kind: "validation" } のように分ければ、種類ごとに違う情報を持てる。
  5. never で網羅性を検証できる。switch にすべての場合を書いたか、コンパイラが確認してくれる。新しい種類を追加したとき、対応漏れが必ず見つかる。

現場使用例:フォーム検証、API呼び出しの失敗、ファイル読み込み、決済処理。「起きて当然の失敗」は Result 型、「起きてはいけない異常」は例外と使い分ける。

// カスタムエラー(種類と追加情報を持てる)
class ValidationError extends Error {
  constructor(public field: string, message: string) {
    super(message);
    this.name = "ValidationError";
  }
}

try {
  throw new ValidationError("name", "必須です");
} catch (e) {
  if (e instanceof ValidationError) {
    console.log(e.field, e.message);   // field にアクセスできる
  }
}

// Result 型(失敗が戻り値の型に現れる)
type Result<T> =
  | { ok: true; value: T }
  | { ok: false; error: string };

function divide(a: number, b: number): Result<number> {
  if (b === 0) {
    return { ok: false, error: "0で除算はできません" };
  }
  return { ok: true, value: a / b };
}

const r = divide(10, 0);
console.log(r.ok ? r.value : r.error);   // 確認しないと使えない

// エラーの種類をタグ付きユニオンで分ける
type AppError =
  | { kind: "network"; status: number }
  | { kind: "validation"; field: string };

function describe(e: AppError): string {
  switch (e.kind) {
    case "network":
      return `通信エラー(${e.status})`;
    case "validation":
      return `入力エラー:${e.field}`;
    default: {
      const _exhaustive: never = e;   // 漏れがあればエラーになる
      return _exhaustive;
    }
  }
}

3基本ドリル(10問)

TS16-D01 Error を継承した ValidationError クラスを定義し(field を追加で持つ)、throw して catch で field と message を出力せよ。 コーディング★☆☆無料
期待される結果

出力 == name / 必須です

ヒント

class ValidationError extends Error { constructor(public field: string, message: string) { super(message); } }

模範解答
class ValidationError extends Error {
  constructor(
    public field: string,
    message: string
  ) {
    super(message);
    this.name = "ValidationError";
  }
}

try {
  throw new ValidationError("name", "必須です");
} catch (e) {
  if (e instanceof ValidationError) {
    console.log(`${e.field} / ${e.message}`);
  }
}
解説

カスタムエラーには追加情報を持たせられる。「どの項目でエラーが起きたか」を、エラー自身が知っている状態にできる。

TS16-D02 function f(): number という関数が例外を投げる可能性は、型から分かるか。
A. 分かる B. 分からない C. 常に投げる D. 投げられない
選択★☆☆無料
期待される結果

解答 == B

ヒント

戻り値の型に例外の情報は含まれるか。

模範解答
B
解説

これが例外の弱点。呼び出す側は型を見ても失敗の可能性に気づけず、try/catch を書き忘れる。Result 型ならこの問題が起きない。

TS16-D03 割り算を行う関数 divide を Result 型で定義せよ(0除算なら失敗)。10÷2 と 10÷0 で試し、「5」「0で除算はできません」と2行出力すること。 コーディング★☆☆無料
期待される結果

出力2行。5 / 0で除算はできません

ヒント

type Result = { ok: true; value: T } | { ok: false; error: string }; を定義する。

模範解答
type Result<T> =
  | { ok: true; value: T }
  | { ok: false; error: string };

function divide(a: number, b: number): Result<number> {
  if (b === 0) {
    return { ok: false, error: "0で除算はできません" };
  }
  return { ok: true, value: a / b };
}

for (const [a, b] of [[10, 2], [10, 0]]) {
  const r = divide(a, b);
  console.log(r.ok ? r.value : r.error);
}
解説

失敗が戻り値の型に含まれているので、呼び出す側は r.ok を確認しないと値を取り出せない。対処忘れが構造的に起きない。

TS16-D04 エラーの種類を判定するコードを完成させよ。 穴埋め★☆☆無料
コード
class MyError extends Error {}

try {
  throw new MyError("失敗");
} catch (e) {
  if (e ____ MyError) {
    console.log("MyErrorです");
  }
}
期待される結果

出力 == MyErrorです

ヒント

クラスのインスタンスかを判定する演算子(TS14)。

模範解答
instanceof
解説

catch の e は unknown なので、必ず種類を確認する。複数のカスタムエラーを使い分けるときも、この判定で分岐する。

TS16-D05 Result 型を使う利点はどれか。
A. 処理が速い B. 失敗の可能性が型に現れる C. コードが短い D. 例外より安全に throw できる
選択★☆☆無料
期待される結果

解答 == B

ヒント

呼び出す側から見て何が変わるか。

模範解答
B
解説

型を見れば「この関数は失敗しうる」と分かる。try/catch の書き忘れという事故が起きなくなる。

TS16-D06 エラーの種類をタグ付きユニオンで定義し(network: status を持つ / validation: field を持つ)、種類ごとに違うメッセージを返す関数 describe を作れ。2種類で試し、2行出力すること。 コーディング★★☆無料
期待される結果

出力2行。通信エラー(404) / 入力エラー:email

ヒント

switch で e.kind により分岐する。それぞれのブロックで固有のプロパティが使える。

模範解答
type AppError =
  | { kind: "network"; status: number }
  | { kind: "validation"; field: string };

function describe(e: AppError): string {
  switch (e.kind) {
    case "network":
      return `通信エラー(${e.status})`;
    case "validation":
      return `入力エラー:${e.field}`;
  }
}

console.log(describe({ kind: "network", status: 404 }));
console.log(describe({ kind: "validation", field: "email" }));
解説

エラーの種類ごとに持つ情報が違うのが実務の実態。タグ付きユニオンなら、通信エラーに field が無いことも型で保証される。

TS16-D07 分岐の網羅性を検証するコードを完成させよ。 穴埋め★★☆無料
コード
type Status = "a" | "b";

function check(s: Status): string {
  switch (s) {
    case "a":
      return "A";
    case "b":
      return "B";
    default: {
      const exhaustive: ____ = s;
      return exhaustive;
    }
  }
}

console.log(check("a"));
期待される結果

出力 == A

ヒント

「決して起こらない」を表す型。小文字5文字。

模範解答
never
解説

すべての場合を処理していれば、default に到達する値は存在しない。だから never 型に代入できる。分岐を書き漏らすと、この行がエラーになって教えてくれる。

TS16-D08 タグ付きユニオンに新しい種類を追加したとき、never による網羅性チェックがあるとどうなるか。
A. 何も起きない B. 対応していない箇所がエラーになる C. 自動で対応される D. 実行時エラーになる
選択★★☆無料
期待される結果

解答 == B

ヒント

新しい種類は default に流れ込む。

模範解答
B
解説

これが網羅性チェックの真価。エラーの種類を1つ追加すれば、対応が必要なすべての箇所がコンパイルエラーで教えてくれる。修正漏れが起きない。

TS16-D09 文字列を数値に変換する関数 toNumber を Result 型で定義せよ(変換できなければ失敗)。"42" と "abc" で試し、「42」「数値に変換できません」と2行出力すること。 コーディング★★☆無料
期待される結果

出力2行。42 / 数値に変換できません

ヒント

Number() の結果を Number.isNaN で確認する。

模範解答
type Result<T> =
  | { ok: true; value: T }
  | { ok: false; error: string };

function toNumber(s: string): Result<number> {
  const n = Number(s);
  if (Number.isNaN(n)) {
    return { ok: false, error: "数値に変換できません" };
  }
  return { ok: true, value: n };
}

for (const s of ["42", "abc"]) {
  const r = toNumber(s);
  console.log(r.ok ? r.value : r.error);
}
解説

「変換できないことは想定内」なので Result 型が適している。例外を投げるほどの異常ではなく、日常的に起こりうる失敗だから。

TS16-D10 2種類のカスタムエラー(NotFoundError と PermissionError)を定義し、それぞれ throw して catch で種類ごとに違うメッセージを出力せよ(2行)。 コーディング★★☆無料
期待される結果

出力2行。見つかりません / 権限がありません

ヒント

それぞれ Error を継承し、instanceof で判別する。

模範解答
class NotFoundError extends Error {}
class PermissionError extends Error {}

function handle(e: unknown): string {
  if (e instanceof NotFoundError) {
    return "見つかりません";
  }
  if (e instanceof PermissionError) {
    return "権限がありません";
  }
  return "不明なエラー";
}

console.log(handle(new NotFoundError()));
console.log(handle(new PermissionError()));
解説

エラーの種類ごとに処理を分けられる。「見つからない」と「権限が無い」では、ユーザーに見せるべきメッセージも次の行動も違う。

4実践シナリオ(5問)

TS16-S01 【フォーム検証】名前を検証する関数 validateName を Result 型で定義せよ(空なら「未入力です」、20文字超なら「長すぎます」)。3通り("田中"、""、21文字)で試し、3行出力すること。 コーディング★★☆無料
期待される結果

出力3行。田中 / 未入力です / 長すぎます

ヒント

trim() してから判定する。21文字は "あ".repeat(21)。

模範解答
type Result<T> =
  | { ok: true; value: T }
  | { ok: false; error: string };

function validateName(name: string): Result<string> {
  const trimmed = name.trim();

  if (trimmed.length === 0) {
    return { ok: false, error: "未入力です" };
  }
  if (trimmed.length > 20) {
    return { ok: false, error: "長すぎます" };
  }
  return { ok: true, value: trimmed };
}

for (const input of ["田中", "", "あ".repeat(21)]) {
  const r = validateName(input);
  console.log(r.ok ? r.value : r.error);
}
解説

フォーム検証は「失敗が当たり前」の処理。例外を投げると try/catch だらけになるので、Result 型のほうが自然に書ける。

TS16-S02 【エラーの分類】3種類のエラー(network/validation/unknown)をタグ付きユニオンで定義し、never による網羅性チェック付きの describe 関数を作れ。3種類で試し、3行出力すること。 コーディング★★☆無料
期待される結果

出力3行。通信エラー(500) / 入力エラー:email / 不明:予期しない問題

ヒント

switch の default で const _: never = e; と書く。

模範解答
type AppError =
  | { kind: "network"; status: number }
  | { kind: "validation"; field: string }
  | { kind: "unknown"; message: string };

function describe(e: AppError): string {
  switch (e.kind) {
    case "network":
      return `通信エラー(${e.status})`;
    case "validation":
      return `入力エラー:${e.field}`;
    case "unknown":
      return `不明:${e.message}`;
    default: {
      const exhaustive: never = e;
      return exhaustive;
    }
  }
}

console.log(describe({ kind: "network", status: 500 }));
console.log(describe({ kind: "validation", field: "email" }));
console.log(describe({ kind: "unknown", message: "予期しない問題" }));
解説

4種類目のエラーを追加すると、この default 行がエラーになる。「対応を忘れている」ことをコンパイラが教えてくれる仕組み。

TS16-S03 【カスタムエラーの活用】在庫不足を表す OutOfStockError(残り在庫数を持つ)を定義し、在庫5に対して10個注文して失敗させ、「在庫不足です(残り5個)」と出力せよ。 コーディング★★☆無料
期待される結果

出力 == 在庫不足です(残り5個)

ヒント

constructor(public remaining: number) で在庫数を保持する。

模範解答
class OutOfStockError extends Error {
  constructor(public remaining: number) {
    super("在庫不足です");
    this.name = "OutOfStockError";
  }
}

function order(stock: number, qty: number): void {
  if (qty > stock) {
    throw new OutOfStockError(stock);
  }
}

try {
  order(5, 10);
} catch (e) {
  if (e instanceof OutOfStockError) {
    console.log(`${e.message}(残り${e.remaining}個)`);
  }
}
解説

エラー自身が「残り何個か」を知っている。呼び出し側で在庫を再取得する必要がなく、ユーザーに具体的な情報を伝えられる。

TS16-S04 【Result のチェーン】文字列を数値に変換し、さらに正の数かを検証する処理を Result 型でつなげよ。"42"、"-5"、"abc" の3通りで試し、「42」「正の数ではありません」「数値に変換できません」と3行出力すること。 コーディング★★★無料
期待される結果

出力3行が完全一致

ヒント

1つ目の Result が失敗ならそのまま返し、成功なら次の検証に進む。

模範解答
type Result<T> =
  | { ok: true; value: T }
  | { ok: false; error: string };

function toNumber(s: string): Result<number> {
  const n = Number(s);
  return Number.isNaN(n)
    ? { ok: false, error: "数値に変換できません" }
    : { ok: true, value: n };
}

function requirePositive(n: number): Result<number> {
  return n > 0
    ? { ok: true, value: n }
    : { ok: false, error: "正の数ではありません" };
}

function parse(s: string): Result<number> {
  const converted = toNumber(s);
  if (!converted.ok) {
    return converted;
  }
  return requirePositive(converted.value);
}

for (const s of ["42", "-5", "abc"]) {
  const r = parse(s);
  console.log(r.ok ? r.value : r.error);
}
解説

失敗したらそこで止めて、そのまま返す——これが Result 型をつなぐ基本形。if (!converted.ok) return converted; の1行で、以降は成功したと確定する。

TS16-S05 【例外を Result に変換】例外を投げる関数を包み、Result 型で返すジェネリックな関数 tryCatch を定義せよ。成功する処理と例外を投げる処理の2通りで試し、「成功:42」「失敗:計算エラー」と2行出力すること。 コーディング★★★無料
期待される結果

出力2行が完全一致

ヒント

function tryCatch(fn: () => T): Result の形。try で fn() を実行する。

模範解答
type Result<T> =
  | { ok: true; value: T }
  | { ok: false; error: string };

function tryCatch<T>(fn: () => T): Result<T> {
  try {
    return { ok: true, value: fn() };
  } catch (e) {
    return {
      ok: false,
      error: e instanceof Error ? e.message : "不明なエラー",
    };
  }
}

const a = tryCatch(() => 42);
console.log(a.ok ? `成功:${a.value}` : `失敗:${a.error}`);

const b = tryCatch<number>(() => {
  throw new Error("計算エラー");
});
console.log(b.ok ? `成功:${b.value}` : `失敗:${b.error}`);
解説

既存のライブラリが例外を投げる場合の橋渡し。1箇所で Result に変換すれば、以降のコードは try/catch を書かずに済む。

5仕上げ課題

TS16-FINAL 【注文処理システム】3種類の失敗を型で扱い分けて、注文処理を実装せよ。

エラーの定義(タグ付きユニオン)
type OrderError =
 | { kind: "invalidQty"; qty: number }
 | { kind: "outOfStock"; remaining: number }
 | { kind: "notFound"; id: number };


Result 型{ ok: true; value: T } | { ok: false; error: OrderError }

処理する注文(4件)
・{ id: 1, qty: 2 } … ノートPC(在庫5・単価128000)→ 成功
・{ id: 1, qty: 10 } … 在庫不足
・{ id: 2, qty: 0 } … 数量が不正
・{ id: 99, qty: 1 } … 商品が存在しない

エラーメッセージnever による網羅性チェックを入れること)
・invalidQty →「数量が不正です(0)」
・outOfStock →「在庫不足です(残り5個)」
・notFound →「商品が見つかりません(ID:99)」

期待される出力(6行)
✅ 256,000円
❌ 在庫不足です(残り5個)
❌ 数量が不正です(0)
❌ 商品が見つかりません(ID:99)
---
成功1件 / 失敗3件
期待される結果

出力6行が完全一致

ヒント

order 関数は Result を返す。検証の順序は「数量→商品の存在→在庫」。describeError で switch し、default に never チェックを入れる。

模範解答
type OrderError =
  | { kind: "invalidQty"; qty: number }
  | { kind: "outOfStock"; remaining: number }
  | { kind: "notFound"; id: number };

type Result<T> =
  | { ok: true; value: T }
  | { ok: false; error: OrderError };

type Product = { id: number; name: string; price: number; stock: number };

const products: Product[] = [
  { id: 1, name: "ノートPC", price: 128000, stock: 5 },
  { id: 2, name: "マウス", price: 3200, stock: 42 },
];

function order(id: number, qty: number): Result<number> {
  if (qty <= 0) {
    return { ok: false, error: { kind: "invalidQty", qty } };
  }

  const product = products.find((p) => p.id === id);
  if (!product) {
    return { ok: false, error: { kind: "notFound", id } };
  }

  if (qty > product.stock) {
    return {
      ok: false,
      error: { kind: "outOfStock", remaining: product.stock },
    };
  }

  return { ok: true, value: product.price * qty };
}

function describeError(e: OrderError): string {
  switch (e.kind) {
    case "invalidQty":
      return `数量が不正です(${e.qty})`;
    case "outOfStock":
      return `在庫不足です(残り${e.remaining}個)`;
    case "notFound":
      return `商品が見つかりません(ID:${e.id})`;
    default: {
      const exhaustive: never = e;
      return exhaustive;
    }
  }
}

const requests = [
  { id: 1, qty: 2 },
  { id: 1, qty: 10 },
  { id: 2, qty: 0 },
  { id: 99, qty: 1 },
];

let ok = 0;
let ng = 0;

for (const req of requests) {
  const result = order(req.id, req.qty);

  if (result.ok) {
    ok++;
    console.log(`✅ ${result.value.toLocaleString()}円`);
    continue;
  }

  ng++;
  console.log(`❌ ${describeError(result.error)}`);
}

console.log("---");
console.log(`成功${ok}件 / 失敗${ng}件`);
解説

3種類の失敗が、それぞれ違う情報を持っている。在庫不足なら「残り何個か」、商品が無いなら「どのIDか」、数量が不正なら「いくつだったか」——ユーザーに伝えるべき情報は、失敗の種類によって違う。タグ付きユニオンなら、それを型として正確に表現できる。

そして describeErrordefault にある const exhaustive: never = e; に注目してほしい。この1行があるおかげで、OrderError に4種類目を追加した瞬間、この関数がコンパイルエラーになる。「新しいエラーを定義したが、メッセージを書き忘れた」という事故が起きない。

例外との違いも明確だ。もし throw で実装していたら、order の型は number のままで、呼び出す側は失敗の可能性に気づけない。try/catch を書き忘れれば、アプリごと落ちる。

「起きて当然の失敗」は Result 型、「起きてはいけない異常」は例外——この使い分けが、堅牢なアプリケーションの土台になる。在庫不足は日常的に起こることであり、異常ではない。だから戻り値で表現するのが正しい。

次章では tsconfig と厳格モードを学ぶ。ここまで暗黙的に有効だった設定を、自分で制御できるようになる。