TypeScript / LESSON 06 / TS06

TypeScript:ユニオン型とリテラル型

1学習の目的

2基礎解説

種類書き方意味
ユニオン型number | string数値または文字列
リテラル型"貸出中" | "返却済"この2つの値のみ
null を含むstring | null文字列または「無し」
絞り込みif (typeof v === "number")その中では number として扱える
✅ 覚えるべき重要ポイント
  1. | で型をつなぐと「どれか1つ」を表すnumber | string は「数値または文字列が入る」という意味。
  2. ユニオン型のままでは共通の操作しかできないv * 2 と書くと「文字列かもしれない」とエラーになる。使う前に型を絞り込む必要がある
  3. リテラル型は「特定の値だけ」を許す型type Status = "貸出中" | "返却済"; と書けば、それ以外の文字列は代入できなくなる。
  4. typeof で確認すると、その中では絞り込まれた型として扱える。if の中では TypeScript が「ここでは number だ」と理解してくれる。
  5. string | null のような型は、null チェックをしないとプロパティにアクセスできない。「値が無いかもしれない」ことを型で表現し、確認を強制できる。

現場使用例:IDが数値か文字列か決まっていないAPI、ステータスの限定("未対応"|"対応中"|"完了")、検索結果が見つからない可能性(User | null)。

// ユニオン型:どちらかが入る
let id: number | string = 1;
id = "A001";   // どちらもOK

// ❌ そのままでは共通の操作しかできない
function bad(v: number | string) {
  return v * 2;   // 文字列かもしれないのでエラー
}

// ✅ 型を絞り込んでから使う
function show(v: number | string): string {
  if (typeof v === "number") {
    return `数値:${v * 2}`;      // ここでは number
  }
  return `文字列:${v.toUpperCase()}`;  // ここでは string
}

// リテラル型:値そのものを限定する
type Status = "貸出中" | "返却済";

const s1: Status = "貸出中";   // OK
const s2: Status = "紛失";     // ❌ エラー

// null を含むユニオン
function find(id: number): string | null {
  return id === 1 ? "見つかった" : null;
}

const result = find(2);
// console.log(result.length);  → Object is possibly 'null'
console.log(result ?? "該当なし");   // ?? で既定値を指定(JS03)

3基本ドリル(10問)

TS06-D01 数値または文字列が入る変数 id を型注釈付きで宣言し、まず 1 を、次に "A001" を代入して、それぞれ出力せよ。 コーディング★☆☆無料
期待される結果

出力2行。1 / A001

ヒント

const ではなく let を使う。型は number | string。

模範解答
let id: number | string = 1;
console.log(id);

id = "A001";
console.log(id);
解説

縦棒 | でつなぐのがユニオン型。どちらの型も代入できるが、それ以外(true など)は代入できない。

TS06-D02 function f(v: number | string) { return v * 2; } と書くとどうなるか。
A. 正しく動く B. エラーになる(文字列かもしれないため) C. 文字列なら連結される D. 数値に変換される
選択★☆☆無料
期待される結果

解答 == B

ヒント

掛け算は数値にしかできない。

模範解答
B
解説

ユニオン型のままでは、両方に共通する操作しかできない。掛け算をしたいなら、先に number であることを確認する必要がある。

TS06-D03 ステータスの型 Status を「未対応」「対応中」「完了」の3つに限定して定義し、「対応中」を代入して出力せよ。 コーディング★☆☆無料
期待される結果

出力 == 対応中

ヒント

type Status = "未対応" | "対応中" | "完了"; と書く。

模範解答
type Status = "未対応" | "対応中" | "完了";

const current: Status = "対応中";

console.log(current);
解説

リテラル型は「この値だけ」を許す型"完了済" のような打ち間違いが、実行前にエラーになる。

TS06-D04 「数値または null」を表す型を補ってコードを完成させよ。 穴埋め★☆☆無料
コード
const found: number ____ null = null;

console.log(found ?? "該当なし");
期待される結果

出力 == 該当なし

ヒント

複数の型をつなぐ記号。縦棒1本。

模範解答
|
解説

?? は null と undefined のときだけ既定値を返す(JS03)。ユニオン型と組み合わせると安全に扱える。

TS06-D05 type S = "A" | "B"; のとき const s: S = "C"; と書くとどうなるか。
A. 代入できる B. エラーになる C. "A" になる D. undefined になる
選択★☆☆無料
期待される結果

解答 == B

ヒント

リテラル型は指定した値だけを許す。

模範解答
B
解説

Type '"C"' is not assignable to type 'S' というエラーになる。TS01のS04で「string型だがどんな文字列でも入る」と指摘した問題が、これで解決した。

TS06-D06 数値または文字列を受け取り、数値なら2倍、文字列なら大文字にして返す関数 process を定義せよ。process(21) と process("abc") の結果を2行で出力すること。 コーディング★★☆無料
期待される結果

出力2行。42 / ABC

ヒント

typeof で分岐する。戻り値の型は number | string になる。

模範解答
function process(v: number | string): number | string {
  if (typeof v === "number") {
    return v * 2;
  }
  return v.toUpperCase();
}

console.log(process(21));
console.log(process("abc"));
解説

if の中では number、外では string として扱える。TypeScriptが文脈から型を絞り込んでくれるので、それぞれの型に固有のメソッドが使える。

TS06-D07 型に名前を付けるキーワードを補ってコードを完成させよ。 穴埋め★★☆無料
コード
____ Rank = "S" | "A" | "B";

const r: Rank = "A";
console.log(r);
期待される結果

出力 == A

ヒント

型に別名を付けるキーワード。小文字で4文字。

模範解答
type
解説

type で型に名前を付けられる。同じ型を何度も書かずに済み、名前自体が意味の説明になる(TS07で詳しく扱う)。

TS06-D08 string | null 型の値のプロパティに、確認せずアクセスするとどうなるか。
A. undefined が返る B. エラーになる(null の可能性がある) C. 空文字が返る D. 問題ない
選択★★☆無料
期待される結果

解答 == B

ヒント

null にはプロパティが無い。

模範解答
B
解説

Object is possibly 'null' というエラーになる。JS07で Cannot read properties of null に悩まされた問題が、書いた時点で防げる。

TS06-D09 商品IDを受け取り、1なら「ノートPC」、それ以外は null を返す関数 findName を定義せよ。findName(1) と findName(99) の結果を、null の場合は「該当なし」と表示して2行出力すること。 コーディング★★☆無料
期待される結果

出力2行。ノートPC / 該当なし

ヒント

戻り値の型は string | null。?? で既定値を指定する。

模範解答
function findName(id: number): string | null {
  return id === 1 ? "ノートPC" : null;
}

console.log(findName(1) ?? "該当なし");
console.log(findName(99) ?? "該当なし");
解説

「見つからないかもしれない」を型で表現している。呼び出す側は null の可能性を無視できないので、対処を忘れることがない。

TS06-D10 リテラル型 Rank("S" | "A" | "B")の配列を宣言し、["S", "A", "B", "A"] のうち "A" の件数を出力せよ。 コーディング★★☆無料
期待される結果

出力 == 2

ヒント

型は Rank[] と書ける。filter で数える。

模範解答
type Rank = "S" | "A" | "B";

const ranks: Rank[] = ["S", "A", "B", "A"];
const countA = ranks.filter((r) => r === "A").length;

console.log(countA);
解説

リテラル型の配列も作れる"C" のような不正な値を混ぜようとすれば、その場でエラーになる。

4実践シナリオ(5問)

TS06-S01 【在庫状態の型付け】在庫数を受け取り、0なら「品切れ」、10未満なら「残りわずか」、それ以上は「在庫あり」を返す関数 stockStatus を定義せよ。戻り値はリテラル型で限定すること。0・5・50 の3通りで試し、3行出力すること。 コーディング★★☆無料
期待される結果

出力3行。品切れ / 残りわずか / 在庫あり

ヒント

type StockStatus = "品切れ" | "残りわずか" | "在庫あり"; と定義し、戻り値の型に使う。

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

function stockStatus(stock: number): StockStatus {
  if (stock === 0) {
    return "品切れ";
  }
  if (stock < 10) {
    return "残りわずか";
  }
  return "在庫あり";
}

console.log(stockStatus(0));
console.log(stockStatus(5));
console.log(stockStatus(50));
解説

戻り値をリテラル型にすると、返せる値が3つに限定される。もし return "在庫切れ"; と書き間違えれば、その場でエラーになる。

TS06-S02 【IDの正規化】数値または文字列のIDを受け取り、必ず文字列の「ID-001」形式(3桁ゼロ埋め)で返す関数 normalizeId を定義せよ。normalizeId(1) と normalizeId("42") の結果を2行出力すること。 コーディング★★☆無料
期待される結果

出力2行。ID-001 / ID-042

ヒント

typeof で分岐し、数値なら文字列に変換する。padStart(3, "0") でゼロ埋めできる。

模範解答
function normalizeId(id: number | string): string {
  const str = typeof id === "number" ? String(id) : id;
  return `ID-${str.padStart(3, "0")}`;
}

console.log(normalizeId(1));
console.log(normalizeId("42"));
解説

ユニオン型を受け取り、1つの型に統一して返す——実務でよくある「正規化」の処理。入口は緩く、出口は厳密にするのが扱いやすい設計。

TS06-S03 【検索と既定値】商品配列(id:1 ノートPC、id:2 マウス)から指定IDの商品を探し、見つからなければ null を返す関数 findProduct を定義せよ。id=2 と id=99 で試し、見つかった場合は「マウス」、見つからない場合は「商品が見つかりません」と2行出力すること。 コーディング★★☆無料
期待される結果

出力2行。マウス / 商品が見つかりません

ヒント

find の結果は undefined になりうるので、?? null で null に統一する。戻り値の型は { id: number; name: string } | null。

模範解答
const products = [
  { id: 1, name: "ノートPC" },
  { id: 2, name: "マウス" },
];

function findProduct(id: number): { id: number; name: string } | null {
  return products.find((p) => p.id === id) ?? null;
}

for (const id of [2, 99]) {
  const found = findProduct(id);
  console.log(found ? found.name : "商品が見つかりません");
}
解説

?? null で undefined を null に統一している。2種類の「無い」が混在すると扱いが煩雑になるので、どちらかに揃えるのが実務の定石。

TS06-S04 【注文ステータスの管理】ステータス型("未発送" | "発送済" | "配達完了")を定義し、ステータスを受け取って次の段階を返す関数 nextStatus を作れ。「配達完了」の場合は null を返すこと。3つのステータスで試し、3行出力すること(null は「完了」と表示)。 コーディング★★★無料
期待される結果

出力3行。発送済 / 配達完了 / 完了

ヒント

戻り値の型は Status | null。switch か if で分岐する。

模範解答
type Status = "未発送" | "発送済" | "配達完了";

function nextStatus(current: Status): Status | null {
  if (current === "未発送") {
    return "発送済";
  }
  if (current === "発送済") {
    return "配達完了";
  }
  return null;
}

for (const s of ["未発送", "発送済", "配達完了"] as Status[]) {
  console.log(nextStatus(s) ?? "完了");
}
解説

状態遷移をリテラル型で表現している。取りうる状態が3つに限定されているので、存在しないステータスへ遷移するコードは書けない。

TS06-S05 【混在データの集計】数値または文字列が混在する配列 [100, "200", 300, "abc"] から、数値として扱える要素だけを合計して出力せよ。文字列は数値に変換を試み、変換できないものは無視すること。 コーディング★★★無料
期待される結果

出力 == 600

ヒント

型は (number | string)[]。typeof で分岐し、文字列は Number() で変換して Number.isNaN() で判定する(JS10)。

模範解答
const values: (number | string)[] = [100, "200", 300, "abc"];

let total = 0;

for (const v of values) {
  if (typeof v === "number") {
    total += v;
    continue;
  }

  const num = Number(v);
  if (!Number.isNaN(num)) {
    total += num;
  }
}

console.log(total);
解説

ユニオン型の配列を、型ごとに処理を分けて扱っているNumber("abc") は NaN になるので、JS10で学んだ Number.isNaN() での判定が必要になる。

5仕上げ課題

TS06-FINAL 【書籍レンタル管理】ユニオン型とリテラル型を使い、レンタル状態を管理するプログラムを実装せよ。

型の定義
Status:「貸出中」「返却済」「延滞」の3つに限定
Book{ id: number; title: string; status: Status; daysOverdue: number | null }
 ※ daysOverdue は延滞日数。延滞していなければ null

データ
・{ id: 1, title: "SQL入門", status: "貸出中", daysOverdue: null }
・{ id: 2, title: "TypeScript実践", status: "延滞", daysOverdue: 5 }
・{ id: 3, title: "JS基礎", status: "返却済", daysOverdue: null }

処理
① 各書籍について「SQL入門:貸出中」の形式で出力する
② ただし延滞している場合は「TypeScript実践:延滞(5日超過・延滞金500円)」と表示(延滞金は1日100円)
③ 最後に「延滞1件/延滞金合計500円」を出力する

期待される出力(4行)
SQL入門:貸出中
TypeScript実践:延滞(5日超過・延滞金500円)
JS基礎:返却済
延滞1件/延滞金合計500円
期待される結果

出力4行が完全一致

ヒント

daysOverdue は number | null なので、そのまま計算するとエラーになる。null チェックを通してから使う。延滞金は daysOverdue * 100。集計は filter や reduce を使うか、ループ内で足し込む。

模範解答
type Status = "貸出中" | "返却済" | "延滞";

type Book = {
  id: number;
  title: string;
  status: Status;
  daysOverdue: number | null;
};

const books: Book[] = [
  { id: 1, title: "SQL入門", status: "貸出中", daysOverdue: null },
  { id: 2, title: "TypeScript実践", status: "延滞", daysOverdue: 5 },
  { id: 3, title: "JS基礎", status: "返却済", daysOverdue: null },
];

let overdueCount = 0;
let totalFee = 0;

for (const book of books) {
  if (book.status === "延滞" && book.daysOverdue !== null) {
    const fee = book.daysOverdue * 100;
    overdueCount++;
    totalFee += fee;
    console.log(
      `${book.title}:延滞(${book.daysOverdue}日超過・延滞金${fee}円)`
    );
    continue;
  }

  console.log(`${book.title}:${book.status}`);
}

console.log(`延滞${overdueCount}件/延滞金合計${totalFee}円`);
解説

この課題の核心は daysOverdue: number | null の扱いだ。「延滞日数は、延滞していなければ存在しない」——この現実を型でそのまま表現している。

重要なのは book.daysOverdue !== null というチェックを書かないとコンパイルが通らないことだ。book.daysOverdue * 100 といきなり書けば、TypeScriptは「null かもしれないものを掛け算しようとしている」と止めてくれる。JavaScriptなら null * 100 が 0 になり、延滞金0円という誤った請求書が静かに出来上がっていた。

そして Status をリテラル型にしたことで、book.status === "延滞中" のような存在しない値との比較も検出される。TS01から「string型だが実際は数種類しか入らない」という状態を放置してきたが、ここでようやく型がデータの実態に追いついた。

型は現実を写し取るためのものだ。「必ずある値」と「無いかもしれない値」、「どんな文字列でも入る項目」と「決まった選択肢しかない項目」——これらを区別して書けるようになると、コードそのものが仕様書として機能し始める。

次章では type と interface を学ぶ。今回 Book 型を定義したように、型に名前を付けて整理する方法を本格的に扱う。