TypeScript / LESSON 15 / TS15

TypeScript:外部データの型付け

1学習の目的

2基礎解説

ここまでの型チェックはコンパイル時のものでした。しかしAPIから来るデータは実行時に決まります。「サーバーが仕様どおり返す」という保証はどこにもありません。

方法書き方安全性
そのまま使うJSON.parse(json)any になる(危険)
型アサーションJSON.parse(json) as Product宣言するだけ(保証なし)
検証関数if (isProduct(data))安全
既定値で補うtypeof o.url === "string" ? … : 既定値安全・壊れない
✅ 覚えるべき重要ポイント
  1. JSON.parse の戻り値は any。そのまま使うと型チェックが一切効かない。TS05で学んだ any の危険がそのまま表れる。
  2. as Product は「そう思ってくれ」という宣言にすぎない。実際のデータが違っても、TypeScriptは信じてしまう。実行時のエラーは防げない
  3. 本当に安全なのは検証関数(型ガード)だけ。TS09で学んだ v is T を使い、プロパティを1つずつ確認する。
  4. 配列は Array.isArray で確認してから filter で絞る。不正な要素を除去すれば、残ったものは安全に扱える。
  5. 「不正なら既定値」という設計も有効。全項目を検証して、欠けていれば既定値で補えば、どんなデータが来ても壊れない。

現場使用例: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問)

TS15-D01 JSON文字列 {"name":"ノートPC","price":128000}JSON.parse で解析し、unknown として受け取ってから型を確認せずに型アサーションで name を出力せよ。 コーディング★☆☆無料
期待される結果

出力 == ノート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は気づかない——次の問題でその危険を見る。

TS15-D02 JSON.parse(json) の戻り値の型はどれか。
A. object B. any C. unknown D. 自動で推論される
選択★☆☆無料
期待される結果

解答 == B

ヒント

中身が何か分からないので、TypeScriptは何を返すか。

模範解答
B
解説

any なので、その後の処理で型チェックが一切効かなくなるdata.存在しない項目 と書いてもエラーにならない。

TS15-D03 値が商品(name: string, price: number)かどうかを判定する型ガード関数 isProduct を定義し、正しいデータと不正なデータで試して2行出力せよ(「有効」「無効」)。 コーディング★☆☆無料
期待される結果

出力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つずつ確認するので、どんなデータが来ても正しく判定できる。

TS15-D04 外部データを安全に受け取る型を補ってコードを完成させよ。 穴埋め★☆☆無料
コード
const raw: ____ = JSON.parse('{"n":1}');

if (typeof raw === "object" && raw !== null) {
  console.log("オブジェクトです");
}
期待される結果

出力 == オブジェクトです

ヒント

any より安全な「型が分からない」を表す型(TS05)。

模範解答
unknown
解説

unknown で受ければ、確認するまで何もできない。any だと確認せずに使えてしまうので、外部データには unknown を使う。

TS15-D05 as Product と書いたとき、実際のデータが Product でなかったらどうなるか。
A. コンパイルエラー B. 実行時にエラー C. TypeScriptが検出する D. 自動変換される
選択★☆☆無料
期待される結果

解答 == B

ヒント

as は実行時に何かをするのか。

模範解答
B
解説

as は型チェックを黙らせるだけで、実行時には何も起きない。データが違えば、使おうとした瞬間に落ちる。

TS15-D06 JSON配列 [{"name":"A","price":100},{"name":"B"}] を解析し、正しい商品だけを型ガードで抽出して件数を出力せよ。 コーディング★★☆無料
期待される結果

出力 == 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件目は自動的に弾かれる。

TS15-D07 配列かどうかを判定するコードを完成させよ。 穴埋め★★☆無料
コード
const raw: unknown = [1, 2, 3];

if (Array.____(raw)) {
  console.log(raw.length);
}
期待される結果

出力 == 3

ヒント

「配列である」を判定する標準メソッド。7文字。

模範解答
isArray
解説

Array.isArray は標準の型ガード。TypeScriptがこれを理解しているので、if の中では配列として扱える。

TS15-D08 外部データを扱うとき、最も安全な方法はどれか。
A. any で受ける B. as で型を宣言する C. 型ガードで検証する D. そのまま使う
選択★★☆無料
期待される結果

解答 == C

ヒント

実行時に確認する方法はどれか。

模範解答
C
解説

実行時に確認するのは型ガードだけ。any も as も、コンパイル時に型チェックを回避するだけで、実際のデータは保証されない。

TS15-D09 不正なJSON文字列 {壊れた を解析しようとして失敗を捕まえ、「解析に失敗しました」と出力せよ。 コーディング★★☆無料
期待される結果

出力 == 解析に失敗しました

ヒント

JSON.parse は SyntaxError を投げる。try/catch で囲む。

模範解答
const json = "{壊れた";

try {
  JSON.parse(json);
  console.log("解析成功");
} catch (e) {
  console.log("解析に失敗しました");
}
解説

JSON.parse は不正な文字列で例外を投げる。外部データを扱うときは、解析そのものが失敗する可能性も考える必要がある。

TS15-D10 設定オブジェクトを検証し、項目が無ければ既定値で補う関数 parseConfig を定義せよ(url は既定値 "https://default.com"、timeout は 30)。url だけ指定した場合の結果を出力すること。 コーディング★★☆無料
期待される結果

出力 == {"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問)

TS15-S01 【APIレスポンスの検証】JSON文字列を受け取り、商品として正しければ「ノートPC:128,000円」、不正なら「不正なデータ」を返す関数 parseProduct を定義せよ。正常なJSONと price が文字列のJSONで試し、2行出力すること。 コーディング★★☆無料
期待される結果

出力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() が呼ばれ、意図しない結果になる。

TS15-S02 【一覧データの検証】JSON配列から正しい商品だけを抽出し、「有効2件(合計131,200円)/無効1件」と出力せよ。データ:ノートPC/128000、マウス/3200、priceが無い商品1件。 コーディング★★☆無料
期待される結果

出力 == 有効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
  }件`
);
解説

不正なデータを除外しつつ、その件数も把握している。実務では「何件が弾かれたか」をログに残すことが重要になる。

TS15-S03 【設定の読み込み】不完全な設定オブジェクトを検証し、既定値で補って完全な Config を返す関数を作れ。3通り(全指定・一部指定・不正な型)で試し、3行出力すること。 コーディング★★☆無料
期待される結果

出力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 が文字列だが、すべて既定値で補われるので処理が止まらない。

TS15-S04 【as の危険性を確かめる】price が文字列で来ている商品を含む配列を as Product[] で受け取り、reduce で価格の合計を計算して出力せよ。何が起きるか観察すること。 コーディング★★★無料
期待される結果

出力 == 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は何も警告しなかった——これが型アサーションの危険性

TS15-S05 【型変換つき検証】price が数値でも文字列でも受け付け、必ず数値に変換して返す関数 normalizeProduct を定義せよ。数値のJSONと文字列のJSONで試し、両方「3,200円」と出力すること。 コーディング★★★無料
期待される結果

出力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仕上げ課題

TS15-FINAL 【APIレスポンスの安全な処理】外部APIから届いたJSONを検証し、レポートを出力せよ。

受け取る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 として扱う
idname が正しくないデータは除外する
・除外された件数も報告する

期待される出力(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 は必ず数値である。「境界で検証し、内側は型を信じる」——これが外部データを扱う設計の基本になる。

次章ではエラー処理と型を学ぶ。失敗を例外ではなく「値」として扱う設計を、本格的に扱う。