TypeScript:外部データの型付け
1学習の目的
- JSON.parse や API から来るデータに型を付ける方法を理解する。型注釈は「保証」ではなく「宣言」でしかないという重要な事実を知る。
- 検証関数を書いて、外部データを安全に扱えるようになる。実行時に型が違っても壊れないコードが書けるようになる。
2基礎解説
ここまでの型チェックはコンパイル時のものでした。しかしAPIから来るデータは実行時に決まります。「サーバーが仕様どおり返す」という保証はどこにもありません。
| 方法 | 書き方 | 安全性 |
|---|---|---|
| そのまま使う | JSON.parse(json) | any になる(危険) |
| 型アサーション | JSON.parse(json) as Product | 宣言するだけ(保証なし) |
| 検証関数 | if (isProduct(data)) | 安全 |
| 既定値で補う | typeof o.url === "string" ? … : 既定値 | 安全・壊れない |
- JSON.parse の戻り値は any。そのまま使うと型チェックが一切効かない。TS05で学んだ any の危険がそのまま表れる。
- as Product は「そう思ってくれ」という宣言にすぎない。実際のデータが違っても、TypeScriptは信じてしまう。実行時のエラーは防げない。
- 本当に安全なのは検証関数(型ガード)だけ。TS09で学んだ v is T を使い、プロパティを1つずつ確認する。
- 配列は Array.isArray で確認してから filter で絞る。不正な要素を除去すれば、残ったものは安全に扱える。
- 「不正なら既定値」という設計も有効。全項目を検証して、欠けていれば既定値で補えば、どんなデータが来ても壊れない。
現場使用例:APIレスポンスの検証、設定ファイルの読み込み、localStorage からの復元、フォームデータの受け取り。「外から来るデータは信用しない」が鉄則。
const json = '{"id":1,"name":"ノートPC","price":128000}';
// ⚠ JSON.parse は any を返す(型チェックが効かない)
const parsed = JSON.parse(json);
console.log(parsed.nmae); // 打ち間違いもエラーにならない
// ⚠ as は「宣言」でしかない(実際の中身は保証されない)
type Product = { id: number; name: string; price: number };
const asserted = JSON.parse(json) as Product;
// もし price が文字列で来ていても、TypeScriptは気づかない
// ✅ 検証関数で確認する(安全)
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"
);
}
const raw: unknown = JSON.parse(json);
if (isProduct(raw)) {
console.log(raw.name); // ここでは Product として扱える
} else {
console.log("不正なデータ");
}
// ✅ 配列は Array.isArray で確認してから filter
const rawList: unknown = JSON.parse('[{"id":1,"name":"A","price":100}]');
const valid = Array.isArray(rawList) ? rawList.filter(isProduct) : [];
3基本ドリル(10問)
出力 == ノートPC
const raw = JSON.parse(json) as { name: string; price: number }; と書く。
const json = '{"name":"ノートPC","price":128000}';
const data = JSON.parse(json) as { name: string; price: number };
console.log(data.name);
as は「この型だと思ってくれ」という宣言。動くが、実際のデータが違っていてもTypeScriptは気づかない——次の問題でその危険を見る。
A. object B. any C. unknown D. 自動で推論される 選択★☆☆無料
解答 == B
中身が何か分からないので、TypeScriptは何を返すか。
B
any なので、その後の処理で型チェックが一切効かなくなる。data.存在しない項目 と書いてもエラーにならない。
出力2行。有効 / 無効
typeof で object かつ null でないことを確認してから、各プロパティの型を調べる。
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";
}
console.log(isProduct({ name: "PC", price: 100 }) ? "有効" : "無効");
console.log(isProduct({ name: "PC" }) ? "有効" : "無効");
これが唯一の確実な方法。実行時に1つずつ確認するので、どんなデータが来ても正しく判定できる。
const raw: ____ = JSON.parse('{"n":1}');
if (typeof raw === "object" && raw !== null) {
console.log("オブジェクトです");
}
出力 == オブジェクトです
any より安全な「型が分からない」を表す型(TS05)。
unknown
unknown で受ければ、確認するまで何もできない。any だと確認せずに使えてしまうので、外部データには unknown を使う。
A. コンパイルエラー B. 実行時にエラー C. TypeScriptが検出する D. 自動変換される 選択★☆☆無料
解答 == B
as は実行時に何かをするのか。
B
as は型チェックを黙らせるだけで、実行時には何も起きない。データが違えば、使おうとした瞬間に落ちる。
出力 == 1
Array.isArray で配列か確認してから filter する。
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 raw: unknown = JSON.parse('[{"name":"A","price":100},{"name":"B"}]');
const valid = Array.isArray(raw) ? raw.filter(isProduct) : [];
console.log(valid.length);
不正な要素を除去すれば、残りは安全に扱える。price が無い2件目は自動的に弾かれる。
const raw: unknown = [1, 2, 3];
if (Array.____(raw)) {
console.log(raw.length);
}
出力 == 3
「配列である」を判定する標準メソッド。7文字。
isArray
Array.isArray は標準の型ガード。TypeScriptがこれを理解しているので、if の中では配列として扱える。
A. any で受ける B. as で型を宣言する C. 型ガードで検証する D. そのまま使う 選択★★☆無料
解答 == C
実行時に確認する方法はどれか。
C
実行時に確認するのは型ガードだけ。any も as も、コンパイル時に型チェックを回避するだけで、実際のデータは保証されない。
出力 == 解析に失敗しました
JSON.parse は SyntaxError を投げる。try/catch で囲む。
const json = "{壊れた";
try {
JSON.parse(json);
console.log("解析成功");
} catch (e) {
console.log("解析に失敗しました");
}
JSON.parse は不正な文字列で例外を投げる。外部データを扱うときは、解析そのものが失敗する可能性も考える必要がある。
出力 == {"url":"https://a.com","timeout":30}
各項目を typeof で確認し、正しければ使い、違えば既定値を返す。
type Config = { url: string; timeout: number };
function parseConfig(v: unknown): Config {
const o = (typeof v === "object" && v !== null ? v : {}) as Record<
string,
unknown
>;
return {
url: typeof o.url === "string" ? o.url : "https://default.com",
timeout: typeof o.timeout === "number" ? o.timeout : 30,
};
}
console.log(JSON.stringify(parseConfig({ url: "https://a.com" })));
「不正なら既定値」という設計は壊れない。どんなデータが来ても必ず Config が返るので、呼び出し側は失敗を考えなくてよい。
4実践シナリオ(5問)
出力2行。ノートPC:128,000円 / 不正なデータ
JSON.parse を try で囲み、結果を型ガードで検証する。
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";
}
function parseProduct(json: string): string {
try {
const raw: unknown = JSON.parse(json);
if (!isProduct(raw)) {
return "不正なデータ";
}
return `${raw.name}:${raw.price.toLocaleString()}円`;
} catch (e) {
return "不正なデータ";
}
}
console.log(parseProduct('{"name":"ノートPC","price":128000}'));
console.log(parseProduct('{"name":"マウス","price":"3200"}'));
price が文字列で来るのは実務でよくある。as Product なら素通りして "3200".toLocaleString() が呼ばれ、意図しない結果になる。
出力 == 有効2件(合計131,200円)/無効1件
Array.isArray で確認し、filter で有効なものを取り出す。無効件数は全体から引く。
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":"ノートPC","price":128000},{"name":"マウス","price":3200},{"name":"不明"}]';
const raw: unknown = JSON.parse(json);
const all = Array.isArray(raw) ? raw : [];
const valid = all.filter(isProduct);
const total = valid.reduce((sum, p) => sum + p.price, 0);
console.log(
`有効${valid.length}件(合計${total.toLocaleString()}円)/無効${
all.length - valid.length
}件`
);
不正なデータを除外しつつ、その件数も把握している。実務では「何件が弾かれたか」をログに残すことが重要になる。
出力3行。全指定/一部/既定値で補完
各項目を typeof で確認し、正しくなければ既定値を使う。
type Config = { url: string; timeout: number; retry: number };
const DEFAULTS: Config = {
url: "https://default.com",
timeout: 30,
retry: 3,
};
function parseConfig(v: unknown): Config {
const o = (typeof v === "object" && v !== null ? v : {}) as Record<
string,
unknown
>;
return {
url: typeof o.url === "string" ? o.url : DEFAULTS.url,
timeout: typeof o.timeout === "number" ? o.timeout : DEFAULTS.timeout,
retry: typeof o.retry === "number" ? o.retry : DEFAULTS.retry,
};
}
const cases: unknown[] = [
{ url: "https://a.com", timeout: 60, retry: 5 },
{ url: "https://b.com" },
{ url: 123, timeout: "遅い" },
];
for (const c of cases) {
const config = parseConfig(c);
console.log(`${config.url} / ${config.timeout}秒 / ${config.retry}回`);
}
型が違う場合も既定値に落ちる。3件目は url が数値、timeout が文字列だが、すべて既定値で補われるので処理が止まらない。
出力 == 1000003200
2件のうち1件の price が文字列。reduce の初期値 0 から始めると、文字列との + で連結が起きる。
type Product = { name: string; price: number };
const json = '[{"name":"A","price":100000},{"name":"B","price":"3200"}]';
const products = JSON.parse(json) as Product[];
const total = products.reduce((sum, p) => sum + p.price, 0);
console.log(total);
期待した 103,200 ではなく 1000003200 という無意味な値になる。reduce の初期値 0 に数値の100000が足されて 100000 になり、次に文字列の "3200" が来た瞬間に連結が始まる。as で number と宣言したのに、TypeScriptは何も警告しなかった——これが型アサーションの危険性。
出力2行。両方とも 3,200円
price が number ならそのまま、string なら Number() で変換し、NaN でないか確認する。
type Product = { name: string; price: number };
function normalizeProduct(v: unknown): Product | null {
if (typeof v !== "object" || v === null) return null;
const o = v as Record<string, unknown>;
if (typeof o.name !== "string") return null;
let price: number;
if (typeof o.price === "number") {
price = o.price;
} else if (typeof o.price === "string") {
const n = Number(o.price);
if (Number.isNaN(n)) return null;
price = n;
} else {
return null;
}
return { name: o.name, price };
}
for (const json of ['{"name":"マウス","price":3200}', '{"name":"マウス","price":"3200"}']) {
const p = normalizeProduct(JSON.parse(json));
console.log(p ? `${p.price.toLocaleString()}円` : "不正");
}
実務では「型が揺れる」APIによく出会う。受け入れる型を広くとり、内部では1つの型に正規化する——この設計なら、外部の事情に振り回されずに済む。
5仕上げ課題
受け取るJSON(意図的に不正なデータを含む)
[
{"id":1,"name":"ノートPC","price":128000,"stock":5},
{"id":2,"name":"マウス","price":"3200","stock":42},
{"id":3,"name":"キーボード"},
{"id":4,"name":"モニター","price":45000,"stock":7}
]
仕様
・price は数値でも文字列でも受け付け、数値に正規化する
・stock が無ければ 0 として扱う
・id と name が正しくないデータは除外する
・除外された件数も報告する
期待される出力(6行)
[1] ノートPC:128,000円(在庫5)
[2] マウス:3,200円(在庫42)
[4] モニター:45,000円(在庫7)
---
有効3件 / 除外1件
在庫総額:1,089,400円
出力6行が完全一致
正規化関数を作り、null を返したものは除外する。price は number/string 両対応、stock は無ければ 0。在庫総額は price × stock の合計。
type Product = { id: number; name: string; price: number; stock: number };
function normalize(v: unknown): Product | null {
if (typeof v !== "object" || v === null) return null;
const o = v as Record<string, unknown>;
if (typeof o.id !== "number" || typeof o.name !== "string") {
return null;
}
let price: number;
if (typeof o.price === "number") {
price = o.price;
} else if (typeof o.price === "string") {
const n = Number(o.price);
if (Number.isNaN(n)) return null;
price = n;
} else {
return null;
}
const stock = typeof o.stock === "number" ? o.stock : 0;
return { id: o.id, name: o.name, price, stock };
}
function isProduct(v: Product | null): v is Product {
return v !== null;
}
const json = `[
{"id":1,"name":"ノートPC","price":128000,"stock":5},
{"id":2,"name":"マウス","price":"3200","stock":42},
{"id":3,"name":"キーボード"},
{"id":4,"name":"モニター","price":45000,"stock":7}
]`;
const raw: unknown = JSON.parse(json);
const all = Array.isArray(raw) ? raw : [];
const products = all.map(normalize).filter(isProduct);
for (const p of products) {
console.log(
`[${p.id}] ${p.name}:${p.price.toLocaleString()}円(在庫${p.stock})`
);
}
console.log("---");
console.log(`有効${products.length}件 / 除外${all.length - products.length}件`);
const totalValue = products.reduce((sum, p) => sum + p.price * p.stock, 0);
console.log(`在庫総額:${totalValue.toLocaleString()}円`);
実際のAPIは、仕様書どおりのデータを返すとは限らない。この課題のJSONには、実務で頻繁に遭遇する3つの問題が仕込まれている。
① price が文字列(id:2)——PHPやRubyのAPIでは数値が文字列になることがよくある。as Product で済ませていたら、"3200" * 42 という計算が走っていた。
② 必須項目の欠落(id:3)——price も stock も無い。除外しなければ、undefined.toLocaleString() で実行時エラーになる。
③ 任意項目の扱い——stock が無い場合に 0 とするか、除外するか。これは業務判断であり、型だけでは決められない。
重要なのは、この処理を通過したデータは、以降どこでも安全に使えるということだ。products は確実に Product[] であり、p.price は必ず数値である。「境界で検証し、内側は型を信じる」——これが外部データを扱う設計の基本になる。
次章ではエラー処理と型を学ぶ。失敗を例外ではなく「値」として扱う設計を、本格的に扱う。