TypeScript:総合演習:型安全な在庫管理
1学習の目的
- TS01〜TS17で学んだすべてを組み合わせ、実務水準の型設計ができるようになる。
- 「どの場面でどの型技法を選ぶか」を判断できるようになる。型注釈だけだった最初の章から、ここまで積み上げてきたものを1つのシステムに収める。
2基礎解説
| 層 | 役割 | 使う道具(学んだ章) |
|---|---|---|
| ① 型定義 | データ構造を表す | type・リテラル型・Utility型(TS06・07・13) |
| ② 境界 | 外部データを検証 | unknown・型ガード(TS09・15) |
| ③ 処理 | 安全に計算・集計 | ジェネリクス・配列操作(TS10・11) |
| ④ 結果 | 失敗を表現する | Result型・never(TS16) |
- まずデータ構造を型で書く。「何があって、何が無いかもしれないか」を ? や | null で正確に表す(TS08)。
- 取りうる値が限られているものはリテラル型にする(TS06)。ステータスや区分を string のままにしない。
- 外部データは境界で検証する(TS15)。一度検証を通せば、内側では型を信じて書ける。
- 失敗する処理は Result 型で表す(TS16)。呼び出し側に確認を強制でき、対処漏れが起きない。
- 共通処理はジェネリクスで書く(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問)
出力 == ノート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種類しか無いことが、読むだけで分かる。
A. stock: number B. stock: number | null C. stock: any D. stock: string 選択★☆☆無料
解答 == B
「値が無いかもしれない」を表す方法(TS08)。
B
0個と未登録は違う状態。number だけでは0で代用するしかなく、意味が混ざってしまう。
出力 == 在庫総額: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が対処を強制してくれる。
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 を確認しないと値を取り出せない。
A. as で型を宣言する B. 型ガードで検証する C. any にする D. そのまま使う 選択★☆☆無料
解答 == B
実行時に確認できる方法はどれか(TS15)。
B
境界で検証すれば、内側では型を信じて書ける。これが型安全なシステムの基本設計。
出力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つに限定される。打ち間違いがその場でエラーになる。
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箇所直せば、派生した型すべてに反映される。同じ項目を何度も書かない設計。
A. 実行が速くなる B. 新しい種類を追加したとき対応漏れが検出される C. コードが短くなる D. エラーが減る 選択★★☆無料
解答 == B
種類を追加したときに何が起きるか(TS16)。
B
修正漏れをコンパイラが教えてくれる。大規模なコードほど、この仕組みの価値が高くなる。
出力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 を扱ったのと同じ問題を、より明示的に解決している。
出力2行。131200 / 47
function sumBy
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問)
出力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つの状態」として扱っている。未登録も正常なデータであり、それを型で表現できている。
出力 == 取込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[] だけを扱えばよく、型を信じて書ける。
出力2行。PC:2件・173,000円 / 周辺機器:1件・3,200円
reduce
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回のループで件数と金額を同時に集められる。
出力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 で一括りにされ、理由の判別が難しくなる。
出力 == 1: 新商品 (5,000円)
Omit
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仕上げ課題
型定義
・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章が、検証・集計・エラー処理を備えた在庫管理システムにたどり着いた。
お疲れさまでした。