TypeScript:ユニオン型とリテラル型
1学習の目的
- ユニオン型で「複数の型のどれか」を表現できるようになる。現実のデータは1つの型に収まらないことが多く、それを正直に型で書けるようになる。
- リテラル型で取りうる値そのものを限定できるようになる。TS01から「string型だが実際は2種類しか入らない」という状態を放置してきたが、ここで解決する。
2基礎解説
| 種類 | 書き方 | 意味 |
|---|---|---|
| ユニオン型 | number | string | 数値または文字列 |
| リテラル型 | "貸出中" | "返却済" | この2つの値のみ |
| null を含む | string | null | 文字列または「無し」 |
| 絞り込み | if (typeof v === "number") | その中では number として扱える |
- | で型をつなぐと「どれか1つ」を表す。number | string は「数値または文字列が入る」という意味。
- ユニオン型のままでは共通の操作しかできない。v * 2 と書くと「文字列かもしれない」とエラーになる。使う前に型を絞り込む必要がある。
- リテラル型は「特定の値だけ」を許す型。type Status = "貸出中" | "返却済"; と書けば、それ以外の文字列は代入できなくなる。
- typeof で確認すると、その中では絞り込まれた型として扱える。if の中では TypeScript が「ここでは number だ」と理解してくれる。
- 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問)
出力2行。1 / A001
const ではなく let を使う。型は number | string。
let id: number | string = 1; console.log(id); id = "A001"; console.log(id);
縦棒 | でつなぐのがユニオン型。どちらの型も代入できるが、それ以外(true など)は代入できない。
A. 正しく動く B. エラーになる(文字列かもしれないため) C. 文字列なら連結される D. 数値に変換される 選択★☆☆無料
解答 == B
掛け算は数値にしかできない。
B
ユニオン型のままでは、両方に共通する操作しかできない。掛け算をしたいなら、先に number であることを確認する必要がある。
出力 == 対応中
type Status = "未対応" | "対応中" | "完了"; と書く。
type Status = "未対応" | "対応中" | "完了"; const current: Status = "対応中"; console.log(current);
リテラル型は「この値だけ」を許す型。"完了済" のような打ち間違いが、実行前にエラーになる。
const found: number ____ null = null; console.log(found ?? "該当なし");
出力 == 該当なし
複数の型をつなぐ記号。縦棒1本。
|
?? は null と undefined のときだけ既定値を返す(JS03)。ユニオン型と組み合わせると安全に扱える。
A. 代入できる B. エラーになる C. "A" になる D. undefined になる 選択★☆☆無料
解答 == B
リテラル型は指定した値だけを許す。
B
Type '"C"' is not assignable to type 'S' というエラーになる。TS01のS04で「string型だがどんな文字列でも入る」と指摘した問題が、これで解決した。
出力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が文脈から型を絞り込んでくれるので、それぞれの型に固有のメソッドが使える。
____ Rank = "S" | "A" | "B"; const r: Rank = "A"; console.log(r);
出力 == A
型に別名を付けるキーワード。小文字で4文字。
type
type で型に名前を付けられる。同じ型を何度も書かずに済み、名前自体が意味の説明になる(TS07で詳しく扱う)。
A. undefined が返る B. エラーになる(null の可能性がある) C. 空文字が返る D. 問題ない 選択★★☆無料
解答 == B
null にはプロパティが無い。
B
Object is possibly 'null' というエラーになる。JS07で Cannot read properties of null に悩まされた問題が、書いた時点で防げる。
出力2行。ノートPC / 該当なし
戻り値の型は string | null。?? で既定値を指定する。
function findName(id: number): string | null {
return id === 1 ? "ノートPC" : null;
}
console.log(findName(1) ?? "該当なし");
console.log(findName(99) ?? "該当なし");
「見つからないかもしれない」を型で表現している。呼び出す側は null の可能性を無視できないので、対処を忘れることがない。
出力 == 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問)
出力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 "在庫切れ"; と書き間違えれば、その場でエラーになる。
出力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つの型に統一して返す——実務でよくある「正規化」の処理。入口は緩く、出口は厳密にするのが扱いやすい設計。
出力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種類の「無い」が混在すると扱いが煩雑になるので、どちらかに揃えるのが実務の定石。
出力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つに限定されているので、存在しないステータスへ遷移するコードは書けない。
出力 == 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仕上げ課題
型の定義
・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 型を定義したように、型に名前を付けて整理する方法を本格的に扱う。