12. ハンズオン

JSON を読んで集計する / any から始めて型を付けていく

📅 作成: 2026-08-29 / 更新: 2026-08-29

この章の到達点

この章の内容

  1. 課題と準備
  2. 4段階で型を付けていく
  3. 解答例と詰まりやすい箇所

12.1 課題と準備

作るもの

売上データの JSON を読み込み、商品ごとの売上合計を降順で出力するスクリプトを作ります。

準備

$ mkdir handson && cd handson
$ npm init -y
$ npm i -D typescript tsx @types/node

sales.json を用意します。

[
	{ "date": "2026-01-15", "product": "ノート", "amount": 1200, "qty": 3 },
	{ "date": "2026-01-16", "product": "ペン", "amount": 300, "qty": 10 },
	{ "date": "2026-02-01", "product": "ノート", "amount": 800, "qty": 2 },
	{ "date": "2026-02-03", "product": "消しゴム", "amount": 150, "qty": 5 },
	{ "date": "2026-02-20", "product": "ペン", "amount": 450, "qty": 15 }
]

手を動かさずに答えだけ見たい場合

この課題の解答は docs/samples/handson/ に置いてあります(step1.tsstep4.tssales.json)。02. 実行環境で触れたとおり、npm i のあと npx tsx step4.ts で動きます。

ただし、まず自分で書いてみてください。この章の目的は完成品を読むことではなく、any を潰していく過程を体験することです。

目指す出力

$ npx tsx step4.ts
ノート: 2000 円
ペン: 750 円
消しゴム: 150 円

いきなり完成形を書こうとしないでください。

実務で TypeScript を導入する場面は、既存の JavaScript に型を足す形が大半です。この課題も同じ順序で進めます。まず動かし、次に型を付け、最後に検証とエラー処理を足します。

12.2 4段階で型を付けていく

段階1 any のまま動かす JSON.parse の戻り値 段階2 型を付ける as Sale[] で仮に通す 段階3 検証を入れる unknown + 型ガード 段階4 エラー処理を整える Result で返す 検出できる範囲 何も検出できない 打ち間違い(プロパティ名) データの不備(型・件数) 対処の強制
図 1 — 段階が進むほど、検出できる問題の範囲が広がる

段階1 — まず動かす

型を気にせず、JavaScript を書く要領で動かします。

// step1.ts
import { readFileSync } from "node:fs";

const text = readFileSync("sales.json", "utf8");
const sales = JSON.parse(text);

const totals: any = {};
for (const s of sales) {
	totals[s.product] = (totals[s.product] ?? 0) + s.amount;
}

console.log(totals);
$ npx tsx step1.ts
{ 'ノート': 2000, 'ペン': 750, '消しゴム': 150 }

動きますが、検査は何も働いていません。

JSON.parse の戻り値は any なので、s.products.amountany です(06)。s.prodcut と打ち間違えても、実行するまで気づけません。

やってみること

段階2 — 型を付ける

データの形を type で書き、集計結果を Map に変えます。

// step2.ts
import { readFileSync } from "node:fs";

type Sale = { date: string; product: string; amount: number; qty: number };

const text = readFileSync("sales.json", "utf8");
const sales = JSON.parse(text) as Sale[];      // ← まだ嘘をついている

function sumByProduct(sales: Sale[]): Map<string, number> {
	const out = new Map<string, number>();
	for (const s of sales) {
		out.set(s.product, (out.get(s.product) ?? 0) + s.amount);
	}
	return out;
}

for (const [product, total] of sumByProduct(sales)) {
	console.log(`${product}: ${total} 円`);
}

打ち間違いは検出できるようになりましたが、as Sale[] が残っています。

06 で見たとおり、as は何も検証していません。JSON の中身が違っていても通ってしまいます。段階3で外します。

やってみること

段階3 — 検証を入れる

as をやめ、unknown で受けて型ガードで確かめます08)。

// step3.ts(抜粋)
function isSale(v: unknown): v is Sale {
	if (typeof v !== "object" || v === null) return false;
	const o = v as Record<string, unknown>;
	return (
		typeof o.date === "string" &&
		typeof o.product === "string" &&
		typeof o.amount === "number" &&
		typeof o.qty === "number"
	);
}

const parsed: unknown = JSON.parse(text);

if (!Array.isArray(parsed)) {
	throw new Error("配列ではありません");
}

const sales: Sale[] = [];
for (let i = 0; i < parsed.length; i++) {
	const row: unknown = parsed[i];
	if (!isSale(row)) {
		throw new Error(`${i} 件目の形式が不正です`);
	}
	sales.push(row);
}

やってみること

段階4 — エラー処理を整える

例外を投げる代わりに Result を返し、呼ぶ側に対処を強制します11)。

段階状態検出できるもの
1any のまま何も検出できない
2型を付けたが as で嘘打ち間違いだけ
3検証を入れたデータの不備も検出
4Result で返す呼ぶ側に対処を強制

12.3 解答例と詰まりやすい箇所

解答例(段階4)

// step4.ts
import { readFileSync } from "node:fs";

type Sale = { date: string; product: string; amount: number; qty: number };

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

function isSale(v: unknown): v is Sale {
	if (typeof v !== "object" || v === null) return false;
	const o = v as Record<string, unknown>;
	return (
		typeof o.date === "string" &&
		typeof o.product === "string" &&
		typeof o.amount === "number" &&
		typeof o.qty === "number"
	);
}

function loadSales(path: string): Result<Sale[]> {
	let text: string;
	try {
		text = readFileSync(path, "utf8");
	} catch {
		return { ok: false, error: new Error(`ファイルを読めません: ${path}`) };
	}

	let parsed: unknown;
	try {
		parsed = JSON.parse(text);
	} catch {
		return { ok: false, error: new Error(`JSON として読めません: ${path}`) };
	}

	if (!Array.isArray(parsed)) {
		return { ok: false, error: new Error("配列ではありません") };
	}

	const sales: Sale[] = [];
	for (let i = 0; i < parsed.length; i++) {
		const row: unknown = parsed[i];
		if (!isSale(row)) {
			return { ok: false, error: new Error(`${i} 件目の形式が不正です`) };
		}
		sales.push(row);
	}
	return { ok: true, value: sales };
}

function sumByProduct(sales: Sale[]): Map<string, number> {
	const out = new Map<string, number>();
	for (const s of sales) {
		out.set(s.product, (out.get(s.product) ?? 0) + s.amount);
	}
	return out;
}

const r = loadSales("sales.json");
if (!r.ok) {
	console.error(`エラー: ${r.error.message}`);
	process.exit(1);
}

for (const [product, total] of [...sumByProduct(r.value)].sort((a, b) => b[1] - a[1])) {
	console.log(`${product}: ${total} 円`);
}

const missing = loadSales("nothing.json");
console.log(missing.ok ? "読めた" : `想定どおり失敗: ${missing.error.message}`);
$ npx tsx step4.ts
ノート: 2000 円
ペン: 750 円
消しゴム: 150 円
想定どおり失敗: ファイルを読めません: nothing.json

配列でない JSON を渡すと、次のように止まります。

$ npx tsx step4.ts          # sales.json が { "not": "an array" } の場合
エラー: 配列ではありません

詰まりやすい箇所

症状原因対処
TS2591: Cannot find name 'node:fs'TS7 types の指定漏れtsconfig.json"types": ["node"]02
parsed[i]any になるArray.isArray の後は any[] になるconst row: unknown = parsed[i]; と明示する
out.get(...)undefined かもしれないstrict Map.getT | undefined を返す?? 0 で既定値を与える(04
process.exit の後もエラーになる型の上では処理が続く扱いprocess.exitnever を返すので通常は不要。出なければ return を足す
ソートの結果が変わらないMap を直接 sort できない[...map] で配列にしてから sort する

発展課題

ここまでで本編は終わりです。

実務で必要になったときは、付録から引いてください。エラーの意味が分からないときは A1. 逆引き、書きたい型が決まっているときは A2. 型レシピ集、処理ごとまとめて欲しいときは A3. 実務パターン集です。