TypeScript を書いていると「この値は文字列か数値のどちらか」「このオプションは 'small'・'medium'・'large' のいずれかだけ許したい」という場面が出てきます。こうした「複数の候補のうちのどれか」を表すのがユニオン型とリテラル型です。この2つを組み合わせると、取りうる値をピンポイントで絞り込み、間違った値をコンパイル時に弾けるようになります。この記事では、ユニオン型の基本、リテラル型との組み合わせ、値によって型を絞り込む「型の絞り込み(narrowing)」、そして実践的な使い方までを初心者向けに解説します。
目次
ユニオン型とは
ユニオン型は、|(縦棒)で型をつないで「このうちのどれか」を表す型です。たとえば「文字列または数値を受け取れる」引数は次のように書きます。
// string または number を受け取れる
function printId(id: string | number) {
console.log(`ID: ${id}`);
}
printId(42); // OK
printId('abc12'); // OK
// printId(true); // エラー: boolean は string | number に代入できない
string | number は「string 型の値、または number 型の値」という意味です。どちらでも受け取れますが、boolean のように候補にない型を渡すとコンパイルエラーになります。any のように「何でも許す」のではなく、許す型を明示的に列挙するのがユニオン型の特徴です。3つ以上つなぐこともでき、string | number | boolean のように書けます。
リテラル型で「特定の値」に限定する
リテラル型は、型として「特定の値そのもの」を指定する書き方です。通常 string は「あらゆる文字列」を表しますが、'small' と書くと「文字列 'small' だけ」という型になります。単体ではあまり意味がありませんが、ユニオン型と組み合わせると強力になります。
// 取りうる値を 3 つの文字列だけに限定する
type Size = 'small' | 'medium' | 'large';
function setSize(size: Size) {
console.log(`サイズを ${size} にしました`);
}
setSize('medium'); // OK
// setSize('big'); // エラー: 'big' は Size に含まれない
Size は 'small'・'medium'・'large' の3つの文字列だけを許す型です。'big' のようにタイプミスや想定外の値を渡すとエラーになるため、「決まった選択肢の中から選ぶ」設定値やモードの表現にぴったりです。エディタの補完でも候補が3つに絞られるので、書き間違いそのものが起きにくくなります。数値のリテラル型(type Dice = 1 | 2 | 3 | 4 | 5 | 6)や真偽値のリテラル型も同じように作れます。
値によって型を絞り込む(narrowing)
ユニオン型の値をそのまま使おうとすると、「どちらの型か分からない」ために操作が制限されます。たとえば string | number の値に対して、文字列専用のメソッド toUpperCase() をいきなり呼ぶことはできません。number のときには存在しないメソッドだからです。
そこで typeof などで「いまどちらの型なのか」を判定すると、その分岐の中では型が一方に絞り込まれ、安全にメソッドを呼べるようになります。これを型の絞り込み(narrowing)と呼びます。
function format(value: string | number): string {
if (typeof value === 'string') {
// このブロックの中では value は string 型に絞り込まれる
return value.toUpperCase();
}
// ここに来た時点で value は number 型
return value.toFixed(2);
}
console.log(format('hello')); // 'HELLO'
console.log(format(3.1)); // '3.10'
typeof value === 'string' が真の分岐では、TypeScript は value を string 型として扱ってくれます。そのため toUpperCase() を安全に呼べます。その分岐を抜けた後は、残る候補は number だけなので、toFixed() が使えます。ユニオン型は「分岐で1つに絞ってから使う」のが基本の流れだと覚えておきましょう。判定には typeof のほか、配列かどうかの Array.isArray()、オブジェクトのプロパティ有無を見る in 演算子などが使えます。
オブジェクトのユニオンと判別プロパティ
オブジェクトの型どうしをユニオンにすることもよくあります。このとき、それぞれの型に共通のプロパティで種類を表す目印を持たせておくと、絞り込みがきれいに書けます。この目印を判別プロパティ(discriminant)と呼びます。
// kind プロパティが種類を表す目印になる
type Circle = { kind: 'circle'; radius: number };
type Rectangle = { kind: 'rectangle'; width: number; height: number };
type Shape = Circle | Rectangle;
function area(shape: Shape): number {
if (shape.kind === 'circle') {
// shape は Circle に絞り込まれ、radius を安全に使える
return Math.PI * shape.radius ** 2;
}
// shape は Rectangle に絞り込まれる
return shape.width * shape.height;
}
console.log(area({ kind: 'circle', radius: 2 }));
kind というリテラル型のプロパティで種類を分けているため、shape.kind === 'circle' の分岐では shape が Circle だと確定し、radius に安全にアクセスできます。Rectangle 側の width を誤って参照しようとすればエラーになります。図形・イベント・API の状態(読み込み中・成功・失敗)など、「種類ごとに持つデータが違う」ものを表すときの定番パターンです。
候補を絞りきれずエラーになるとき
ユニオン型でよく出るエラーが「プロパティ 'xxx' は型 'A | B' に存在しません」です。これはまだ型を1つに絞り込めていないのに、片方だけが持つプロパティやメソッドを使おうとしているときに起こります。
function toUpper(value: string | number) {
// エラー: toUpperCase は string にしかない
return value.toUpperCase();
}
解決策は、これまで見てきたように typeof などで分岐して型を絞り込むことです。ユニオン型の値に対して直接使えるのは、すべての候補に共通して存在するメンバーだけだと理解しておくと、このエラーの原因がすぐ分かります。たとえば string | number なら、両方が持つ toString() はそのまま呼べますが、片方にしかない toUpperCase() は絞り込みが必要、というわけです。
まとめ
ユニオン型は string | number のように | で型をつなぎ、「このうちのどれか」を表す型です。リテラル型('small' | 'medium' | 'large' など特定の値そのものを型にしたもの)と組み合わせると、取りうる値を必要な候補だけに絞り込め、想定外の値をコンパイル時に防げます。ユニオン型の値は、typeof や in、判別プロパティによる分岐で1つの型に絞り込んでから使うのが基本です。オブジェクトのユニオンでは kind のような目印を用意すると、絞り込みがきれいに書けます。「取りうる値をきちんと列挙して、使うときは分岐で絞る」という流れを意識すると、より安全で読みやすい TypeScript が書けるようになります。