TypeScript / LESSON 18 / TS18

TypeScript:総合演習:型安全な在庫管理

1学習の目的

2基礎解説

役割使う道具(学んだ章)
① 型定義データ構造を表すtype・リテラル型・Utility型(TS06・07・13)
② 境界外部データを検証unknown・型ガード(TS09・15)
③ 処理安全に計算・集計ジェネリクス・配列操作(TS10・11)
④ 結果失敗を表現するResult型・never(TS16)
✅ 型設計を組み立てる手順
  1. まずデータ構造を型で書く。「何があって、何が無いかもしれないか」を ?| null で正確に表す(TS08)。
  2. 取りうる値が限られているものはリテラル型にする(TS06)。ステータスや区分を string のままにしない。
  3. 外部データは境界で検証する(TS15)。一度検証を通せば、内側では型を信じて書ける。
  4. 失敗する処理は Result 型で表す(TS16)。呼び出し側に確認を強制でき、対処漏れが起きない。
  5. 共通処理はジェネリクスで書く(TS10)。any を使わずに再利用できる部品になる。

この章の題材:商品マスタ・在庫・注文を扱う在庫管理システムを、型安全に構築します。

// ① 型定義:現実をそのまま表す
type Category = "PC" | "周辺機器";        // 取りうる値を限定(TS06)

type Product = {
  id: number;
  name: string;
  price: number;
  category: Category;
  stock: number | null;                   // 未登録がありうる(TS08)
};

// ② Result 型:失敗を値として扱う(TS16)
type Result<T> =
  | { ok: true; value: T }
  | { ok: false; error: string };

// ③ ジェネリックな部品(TS10)
function sumBy<T>(items: T[], fn: (item: T) => number): number {
  return items.reduce((sum, i) => sum + fn(i), 0);
}

// ④ Utility型で用途別の型を派生(TS13)
type ProductSummary = Pick<Product, "id" | "name" | "price">;
type ProductInput = Omit<Product, "id">;

3基本ドリル(10問)

TS18-D01 Category("PC" | "周辺機器")と Product(id, name, price, category)を型定義し、1件作って「ノートPC [PC]」と出力せよ。 コーディング★☆☆無料
期待される結果

出力 == ノートPC [PC]

ヒント

リテラル型を type で定義し、Product の中で使う(TS06・07)。

模範解答
type Category = "PC" | "周辺機器";

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

const item: Product = {
  id: 1,
  name: "ノートPC",
  price: 128000,
  category: "PC",
};

console.log(`${item.name} [${item.category}]`);
解説

型定義がそのままデータ仕様書になる。カテゴリが2種類しか無いことが、読むだけで分かる。

TS18-D02 「在庫が未登録の商品がある」ことを型で表すのに最も適した書き方はどれか。
A. stock: number B. stock: number | null C. stock: any D. stock: string
選択★☆☆無料
期待される結果

解答 == B

ヒント

「値が無いかもしれない」を表す方法(TS08)。

模範解答
B
解説

0個と未登録は違う状態number だけでは0で代用するしかなく、意味が混ざってしまう。

TS18-D03 商品配列から在庫金額の合計(price × stock、stockがnullなら0)を計算し、「在庫総額:640,000円」と出力せよ(ノートPC/128000/5、マウス/3200/null)。 コーディング★☆☆無料
期待される結果

出力 == 在庫総額:640,000円

ヒント

?? 0 で null を0に置き換えてから計算する(TS08)。

模範解答
type Product = { name: string; price: number; stock: number | null };

const items: Product[] = [
  { name: "ノートPC", price: 128000, stock: 5 },
  { name: "マウス", price: 3200, stock: null },
];

const total = items.reduce((sum, i) => sum + i.price * (i.stock ?? 0), 0);

console.log(`在庫総額:${total.toLocaleString()}円`);
解説

?? 0 が無いとコンパイルが通らない。null との掛け算はできないので、TypeScriptが対処を強制してくれる。

TS18-D04 Result 型を定義するコードを完成させよ。 穴埋め★☆☆無料
コード
type Result<T> =
  | { ok: true; value: T }
  ____ { ok: false; error: string };

function show(r: Result<number>): void {
  console.log(r.ok ? r.value : r.error);
}

show({ ok: true, value: 42 });
期待される結果

出力 == 42

ヒント

複数の型をつなぐ記号(TS06)。

模範解答
|
解説

成功と失敗を1つの型で表す。呼び出し側は ok を確認しないと値を取り出せない。

TS18-D05 外部APIから来たデータを扱うとき、最初にすべきことはどれか。
A. as で型を宣言する B. 型ガードで検証する C. any にする D. そのまま使う
選択★☆☆無料
期待される結果

解答 == B

ヒント

実行時に確認できる方法はどれか(TS15)。

模範解答
B
解説

境界で検証すれば、内側では型を信じて書ける。これが型安全なシステムの基本設計。

TS18-D06 在庫状態を返す関数 stockLabel をリテラル型の戻り値で定義せよ(nullなら「未登録」、0なら「品切れ」、10未満なら「残りわずか」、それ以外「在庫あり」)。4通りで試し、4行出力すること。 コーディング★★☆無料
期待される結果

出力4行。未登録 / 品切れ / 残りわずか / 在庫あり

ヒント

戻り値の型をリテラル型のユニオンにする(TS06)。null判定を最初に書く(TS13)。

模範解答
type StockLabel = "未登録" | "品切れ" | "残りわずか" | "在庫あり";

function stockLabel(stock: number | null): StockLabel {
  if (stock === null) return "未登録";
  if (stock === 0) return "品切れ";
  if (stock < 10) return "残りわずか";
  return "在庫あり";
}

for (const s of [null, 0, 5, 50]) {
  console.log(stockLabel(s));
}
解説

戻り値をリテラル型にすると、返せる値が4つに限定される。打ち間違いがその場でエラーになる。

TS18-D07 用途別の型を派生させるコードを完成させよ。 穴埋め★★☆無料
コード
type Product = { id: number; name: string; price: number };

type Summary = ____<Product, "id" | "name">;

const s: Summary = { id: 1, name: "PC" };
console.log(Object.keys(s).join(","));
期待される結果

出力 == id,name

ヒント

指定した項目だけを取り出すUtility型(TS13)。4文字。

模範解答
Pick
解説

元の型を1箇所直せば、派生した型すべてに反映される。同じ項目を何度も書かない設計。

TS18-D08 タグ付きユニオンに never の網羅性チェックを入れる利点はどれか。
A. 実行が速くなる B. 新しい種類を追加したとき対応漏れが検出される C. コードが短くなる D. エラーが減る
選択★★☆無料
期待される結果

解答 == B

ヒント

種類を追加したときに何が起きるか(TS16)。

模範解答
B
解説

修正漏れをコンパイラが教えてくれる。大規模なコードほど、この仕組みの価値が高くなる。

TS18-D09 商品を検索し、Result 型で返す関数 findProduct を定義せよ。id=1(見つかる)と id=99(見つからない)で試し、「ノートPC」「商品が見つかりません」と2行出力すること。 コーディング★★☆無料
期待される結果

出力2行。ノートPC / 商品が見つかりません

ヒント

find の結果が undefined なら失敗を返す(TS16)。

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

const items: Product[] = [{ id: 1, name: "ノートPC" }];

function findProduct(id: number): Result<Product> {
  const found = items.find((i) => i.id === id);
  return found
    ? { ok: true, value: found }
    : { ok: false, error: "商品が見つかりません" };
}

for (const id of [1, 99]) {
  const r = findProduct(id);
  console.log(r.ok ? r.value.name : r.error);
}
解説

失敗が型に現れているので、呼び出し側は必ず確認する。TS11で undefined を扱ったのと同じ問題を、より明示的に解決している。

TS18-D10 ジェネリックな集計関数 sumBy を定義し、商品配列から価格の合計と在庫数の合計を求めて2行出力せよ(ノートPC/128000/5、マウス/3200/42)。 コーディング★★☆無料
期待される結果

出力2行。131200 / 47

ヒント

function sumBy(items: T[], fn: (item: T) => number): number と書く(TS10)。

模範解答
function sumBy<T>(items: T[], fn: (item: T) => number): number {
  return items.reduce((sum, i) => sum + fn(i), 0);
}

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

console.log(sumBy(products, (p) => p.price));
console.log(sumBy(products, (p) => p.stock));
解説

1つの関数で何を合計するかを外から指定できる。any を使わずに汎用性を確保できるのがジェネリクスの価値。

4実践シナリオ(5問)

TS18-S01 【在庫レポート】商品配列(在庫にnullを含む)から、各商品を「ノートPC:残りわずか(5個)」の形式で出力し、最後に「未登録1件」と報告せよ(ノートPC/5、マウス/null、サンプル/0)。 コーディング★★☆無料
期待される結果

出力4行が完全一致

ヒント

stockLabel で状態を判定し、null の場合は個数表示を変える。

模範解答
type Product = { name: string; stock: number | null };

function stockLabel(stock: number | null): string {
  if (stock === null) return "未登録";
  if (stock === 0) return "品切れ";
  if (stock < 10) return "残りわずか";
  return "在庫あり";
}

const items: Product[] = [
  { name: "ノートPC", stock: 5 },
  { name: "マウス", stock: null },
  { name: "サンプル", stock: 0 },
];

for (const i of items) {
  const count = i.stock === null ? "" : `(${i.stock}個)`;
  console.log(`${i.name}:${stockLabel(i.stock)}${count}`);
}

const unregistered = items.filter((i) => i.stock === null).length;
console.log(`未登録${unregistered}件`);
解説

null を「例外」ではなく「1つの状態」として扱っている。未登録も正常なデータであり、それを型で表現できている。

TS18-S02 【外部データの取り込み】JSON配列を検証し、正しい商品だけを取り込んで「取込2件 / 除外1件」と出力せよ(priceが文字列の商品を含む3件)。 コーディング★★☆無料
期待される結果

出力 == 取込2件 / 除外1件

ヒント

型ガードで検証し、filter で絞る(TS15)。

模範解答
type Product = { name: string; price: number };

function isProduct(v: unknown): v is Product {
  if (typeof v !== "object" || v === null) return false;
  const o = v as Record<string, unknown>;
  return typeof o.name === "string" && typeof o.price === "number";
}

const json =
  '[{"name":"A","price":100},{"name":"B","price":"200"},{"name":"C","price":300}]';

const raw: unknown = JSON.parse(json);
const all = Array.isArray(raw) ? raw : [];
const valid = all.filter(isProduct);

console.log(`取込${valid.length}件 / 除外${all.length - valid.length}件`);
解説

境界で不正なデータを弾いている。この先の処理は Product[] だけを扱えばよく、型を信じて書ける。

TS18-S03 【カテゴリ別集計】商品配列をカテゴリ別に集計し、「PC:2件・173,000円」の形式で出力せよ(ノートPC/PC/128000、マウス/周辺機器/3200、モニター/PC/45000)。 コーディング★★☆無料
期待される結果

出力2行。PC:2件・173,000円 / 周辺機器:1件・3,200円

ヒント

reduce> でカテゴリ別に件数と金額を集める(TS11・13)。

模範解答
type Category = "PC" | "周辺機器";
type Product = { name: string; category: Category; price: number };

const items: Product[] = [
  { name: "ノートPC", category: "PC", price: 128000 },
  { name: "マウス", category: "周辺機器", price: 3200 },
  { name: "モニター", category: "PC", price: 45000 },
];

const summary = items.reduce<Record<string, { count: number; total: number }>>(
  (acc, i) => {
    const current = acc[i.category] ?? { count: 0, total: 0 };
    acc[i.category] = {
      count: current.count + 1,
      total: current.total + i.price,
    };
    return acc;
  },
  {}
);

for (const [category, s] of Object.entries(summary)) {
  console.log(`${category}:${s.count}件・${s.total.toLocaleString()}円`);
}
解説

集計結果もオブジェクトの型で表現しているRecord の値に複数の情報を持たせることで、1回のループで件数と金額を同時に集められる。

TS18-S04 【注文処理】Result 型で注文を処理する関数 order を作れ(数量0以下・在庫不足・商品なしで失敗)。4件の注文を処理し、成功と失敗を4行+集計1行で出力せよ。 コーディング★★★無料
期待される結果

出力5行。成功1件 / 失敗3件

ヒント

3種類の失敗をそれぞれ Result で返す(TS16)。

模範解答
type Product = { id: number; name: string; price: number; stock: number };
type Result<T> =
  | { ok: true; value: T }
  | { ok: false; error: string };

const products: Product[] = [
  { id: 1, name: "ノートPC", price: 128000, stock: 5 },
];

function order(id: number, qty: number): Result<number> {
  if (qty <= 0) return { ok: false, error: "数量が不正です" };

  const p = products.find((x) => x.id === id);
  if (!p) return { ok: false, error: "商品が見つかりません" };

  if (qty > p.stock) return { ok: false, error: "在庫が不足しています" };

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

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

let ok = 0;
let ng = 0;

for (const r of requests) {
  const result = order(r.id, r.qty);
  if (result.ok) {
    ok++;
    console.log(`✅ ${result.value.toLocaleString()}円`);
    continue;
  }
  ng++;
  console.log(`❌ ${result.error}`);
}

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

失敗の理由が呼び出し側に正確に伝わる。例外なら try/catch で一括りにされ、理由の判別が難しくなる。

TS18-S05 【Utility型の活用】Product 型から一覧用(Pick)と登録用(Omit)の型を派生させ、新規登録して一覧形式で出力せよ。「1: 新商品 (5,000円)」と出力すること。 コーディング★★★無料
期待される結果

出力 == 1: 新商品 (5,000円)

ヒント

Omit で受け取り、id を付けて Product にする(TS13)。

模範解答
type Product = {
  id: number;
  name: string;
  price: number;
  category: string;
  stock: number;
};

type ProductInput = Omit<Product, "id">;
type ProductSummary = Pick<Product, "id" | "name" | "price">;

function create(input: ProductInput, id: number): Product {
  return { id, ...input };
}

function summarize(p: Product): ProductSummary {
  return { id: p.id, name: p.name, price: p.price };
}

const created = create(
  { name: "新商品", price: 5000, category: "PC", stock: 10 },
  1
);

const s = summarize(created);
console.log(`${s.id}: ${s.name} (${s.price.toLocaleString()}円)`);
解説

1つの型から用途別の型が派生している。Product に項目を追加すれば、両方に自動で反映される。

5仕上げ課題

TS18-FINAL 【在庫管理システム】TypeScript編の卒業課題。ここまでの17章すべてを使い、在庫管理システムを完成させよ。

型定義
Category:「PC」「周辺機器」(リテラル型)
Product{ id, name, price, category, stock: number | null }
Result<T>:成功/失敗のタグ付きユニオン

外部から届くJSON(不正なデータを含む)
[
 {"id":1,"name":"ノートPC","price":128000,"category":"PC","stock":5},
 {"id":2,"name":"マウス","price":3200,"category":"周辺機器","stock":null},
 {"id":3,"name":"不正品","price":"5000","category":"PC","stock":1},
 {"id":4,"name":"モニター","price":45000,"category":"PC","stock":0}
]


処理
① JSONを検証して取り込む(price が文字列のものは除外)
② 各商品を「[PC] ノートPC:128,000円(残りわずか)」の形式で出力
 在庫ラベル:null→未登録/0→品切れ/10未満→残りわずか/以上→在庫あり
③ 在庫総額を計算(stock が null は0として計算)
④ id=1 に 2個の注文を処理(Result型)
⑤ id=4 に 1個の注文を処理(在庫0なので失敗)

期待される出力(8行)
=== 在庫一覧 ===
[PC] ノートPC:128,000円(残りわずか)
[周辺機器] マウス:3,200円(未登録)
[PC] モニター:45,000円(品切れ)
取込3件 / 除外1件
在庫総額:640,000円
--- 注文処理 ---
✅ 256,000円
❌ 在庫が不足しています
期待される結果

出力9行が完全一致

ヒント

型ガードで検証する際、category はリテラル型なので "PC" または "周辺機器" かも確認する。在庫総額は price × (stock ?? 0)。注文は Result 型で返し、在庫null・在庫不足・商品なしを失敗とする。

模範解答
type Category = "PC" | "周辺機器";

type Product = {
  id: number;
  name: string;
  price: number;
  category: Category;
  stock: number | null;
};

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

function isCategory(v: unknown): v is Category {
  return v === "PC" || v === "周辺機器";
}

function isProduct(v: unknown): v is Product {
  if (typeof v !== "object" || v === null) return false;

  const o = v as Record<string, unknown>;

  return (
    typeof o.id === "number" &&
    typeof o.name === "string" &&
    typeof o.price === "number" &&
    isCategory(o.category) &&
    (typeof o.stock === "number" || o.stock === null)
  );
}

function stockLabel(stock: number | null): string {
  if (stock === null) return "未登録";
  if (stock === 0) return "品切れ";
  if (stock < 10) return "残りわずか";
  return "在庫あり";
}

function order(products: Product[], id: number, qty: number): Result<number> {
  if (qty <= 0) {
    return { ok: false, error: "数量が不正です" };
  }

  const p = products.find((x) => x.id === id);
  if (!p) {
    return { ok: false, error: "商品が見つかりません" };
  }

  if (p.stock === null) {
    return { ok: false, error: "在庫が未登録です" };
  }

  if (qty > p.stock) {
    return { ok: false, error: "在庫が不足しています" };
  }

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

const json = `[
  {"id":1,"name":"ノートPC","price":128000,"category":"PC","stock":5},
  {"id":2,"name":"マウス","price":3200,"category":"周辺機器","stock":null},
  {"id":3,"name":"不正品","price":"5000","category":"PC","stock":1},
  {"id":4,"name":"モニター","price":45000,"category":"PC","stock":0}
]`;

const raw: unknown = JSON.parse(json);
const all = Array.isArray(raw) ? raw : [];
const products = all.filter(isProduct);

console.log("=== 在庫一覧 ===");

for (const p of products) {
  console.log(
    `[${p.category}] ${p.name}:${p.price.toLocaleString()}円(${stockLabel(
      p.stock
    )})`
  );
}

console.log(`取込${products.length}件 / 除外${all.length - products.length}件`);

const totalValue = products.reduce(
  (sum, p) => sum + p.price * (p.stock ?? 0),
  0
);
console.log(`在庫総額:${totalValue.toLocaleString()}円`);

console.log("--- 注文処理 ---");

for (const req of [
  { id: 1, qty: 2 },
  { id: 4, qty: 1 },
]) {
  const result = order(products, req.id, req.qty);
  console.log(
    result.ok ? `✅ ${result.value.toLocaleString()}円` : `❌ ${result.error}`
  );
}
解説

これがTypeScript編の到達点だ。この1本のプログラムには、TS01からTS17までのほぼすべてが詰まっている。

リテラル型(Category)が不正な区分を弾き、ユニオン型number | null)が「未登録の在庫」を正確に表す。型ガードが外部データを境界で検証し、Result型が注文の失敗を値として返す。ジェネリクスが Result を任意の型で再利用可能にし、厳格モードがすべての null チェックを強制している。

とくに isProduct の中で isCategory(o.category) を呼んでいる点に注目してほしい。「PC」でも「周辺機器」でもない文字列が来たら、その商品ごと除外される。型定義が category: string だったら、この検証は書けなかった——型を厳密にすることが、検証を厳密にする

そして最も重要なのは、この検証を通過した products は、以降どこでも安全に使えることだ。p.price は必ず数値であり、p.category は必ず2つのうちどちらかであり、p.stock は数値か null のいずれかである。境界で守りを固めれば、内側は型を信じて素直に書ける。

——JavaScript編で学んだ落とし穴を思い出してほしい。undefined が紛れ込む、await を忘れる、null を参照して落ちる。そのほとんどが、TypeScriptでは書いた時点で検出される。18章かけて学んできたのは、新しい言語ではなく、間違いを実行前に見つける仕組みだった。

ここまで来たあなたは、もう「TypeScriptを勉強している人」ではありません。型でシステムを設計できる人です。型注釈1つから始まったこの18章が、検証・集計・エラー処理を備えた在庫管理システムにたどり着いた。

お疲れさまでした。