Union型・リテラル型・enum
この章では、複数の型を組み合わせるUnion型、特定の値のみを許可するリテラル型、そして関連する定数をまとめるenumについて学びます。
この章で学ぶこと
- Union型で複数の型を許容する方法
- リテラル型で特定の値のみを許可する方法
- 列挙型(enum)の使い方
- 実践的な型設計のパターン
この章の内容は、より堅牢な型を設計するために重要です。Union型とリテラル型を組み合わせることで、許容される値を厳密に制限できます。
リテラル型
リテラル型は、特定の値のみを許可する型です。
基本的なリテラル型
// 文字列リテラル型
let direction: 'north' = 'north';
// direction = 'south'; // Error: 'north'以外は代入できない
// 数値リテラル型
let statusCode: 200 = 200;
// statusCode = 404; // Error: 200以外は代入できない
// 真偽値リテラル型
let isTrue: true = true;
// isTrue = false; // Error: true以外は代入できない
letとconstでの型推論の違い
// let: 幅広い型に推論される
let str1 = 'hello'; // 型はstring
str1 = 'world'; // OK(任意のstring値を代入できる)
// const: リテラル型に推論される
const str2 = 'hello'; // 型は'hello'(文字列リテラル型)
// str2 = 'world'; // Error: constなので再代入不可
as const(const assertion)
as constを使うと、オブジェクトや配列の値をリテラル型として固定できます。
// 通常のオブジェクト
let config1 = {
apiUrl: 'https://api.example.com',
timeout: 5000
};
// config1の型: { apiUrl: string; timeout: number; }
config1.apiUrl = 'https://api2.example.com'; // OK(変更可能)
// as constを使用
let config2 = {
apiUrl: 'https://api.example.com',
timeout: 5000
} as const;
// config2の型:
// {
// readonly apiUrl: 'https://api.example.com';
// readonly timeout: 5000;
// }
// config2.apiUrl = 'https://api2.example.com'; // Error(readonly)
// config2.timeout = 3000; // Error(readonly)
as constの効果
- すべてのプロパティがreadonlyになる
- プリミティブ値はリテラル型になる
- 配列はreadonlyタプルになる
// 配列でのas const
let arr1 = [1, 2, 3];
// 型: number[]
let arr2 = [1, 2, 3] as const;
// 型: readonly [1, 2, 3](readonlyタプル)
// arr2.push(4); // Error(readonlyなので変更不可)
// arr2[0] = 10; // Error(readonlyなので変更不可)
Union型(複数の型の組み合わせ)
Union型は、複数の型のいずれかであることを表す型です。
基本的なUnion型
// 基本的な構文: 型1 | 型2 | 型3
let value: string | number;
value = 'Hello'; // OK(string)
value = 42; // OK(number)
// value = true; // Error(booleanは許可されていない)
リテラル型とUnion型の組み合わせ
リテラル型とUnion型を組み合わせると、許容される値を限定できます。
// 方向を表す型
type Direction = 'north' | 'south' | 'east' | 'west';
let dir: Direction = 'north'; // OK
dir = 'south'; // OK
// dir = 'up'; // Error: 'up'は許可されていない
// サイズを表す型
type Size = 'small' | 'medium' | 'large';
let shirtSize: Size = 'medium'; // OK
// shirtSize = 'extra-large'; // Error
// HTTPステータスコード
type SuccessStatusCode = 200 | 201 | 204;
type ErrorStatusCode = 400 | 404 | 500;
type StatusCode = SuccessStatusCode | ErrorStatusCode;
let status: StatusCode = 200; // OK
status = 404; // OK
// status = 301; // Error
実践的な使用例
// 例1: イベントの種類
type MouseEventType = 'click' | 'dblclick' | 'mousedown' | 'mouseup';
type KeyboardEventType = 'keydown' | 'keyup' | 'keypress';
type EventType = MouseEventType | KeyboardEventType;
function handleEvent(eventType: EventType): void {
console.log(`Handling event: ${eventType}`);
}
handleEvent('click'); // OK
handleEvent('keydown'); // OK
// handleEvent('scroll'); // Error
// 例2: テーマと言語の設定
type ThemeMode = 'light' | 'dark' | 'auto';
type Language = 'ja' | 'en' | 'zh';
type AppConfig = {
theme: ThemeMode;
language: Language;
};
let config: AppConfig = {
theme: 'dark',
language: 'ja'
};
// 例3: 曜日
type DayOfWeek = 'Sunday' | 'Monday' | 'Tuesday' | 'Wednesday' | 'Thursday' | 'Friday' | 'Saturday';
function isWeekend(day: DayOfWeek): boolean {
return day === 'Sunday' || day === 'Saturday';
}
console.log(isWeekend('Monday')); // false
console.log(isWeekend('Saturday')); // true
型の絞り込み(Type Narrowing)
Union型の変数を使う際は、型を絞り込んでから操作します。
function processValue(value: string | number): void {
// このままでは、stringとnumberの共通のメソッドしか使えない
// console.log(value.toUpperCase()); // Error: numberにはtoUpperCaseがない
// typeof型ガードで型を絞り込む
if (typeof value === 'string') {
// この中ではvalueはstring型として扱える
console.log(value.toUpperCase());
} else {
// この中ではvalueはnumber型として扱える
console.log(value.toFixed(2));
}
}
processValue('hello'); // 'HELLO'
processValue(3.14159); // '3.14'
配列とUnion型
// 配列の要素がUnion型
let mixedArray: (string | number)[] = [1, 'two', 3, 'four'];
mixedArray.push(5); // OK
mixedArray.push('six'); // OK
// mixedArray.push(true); // Error
// 配列自体がUnion型
let arrayOrString: number[] | string = [1, 2, 3];
arrayOrString = 'hello'; // OK
// arrayOrString = 123; // Error
列挙型(enum)
列挙型は、関連する定数をひとまとめにする機能です。
2026年の潮流: enum は新規コードで避けるのが推奨
TypeScript 5.8+ で導入された erasableSyntaxOnly オプション(17章 モジュールとtsconfig 参照)では enum がエラーになります。これは enum が「型だけを消せば JavaScript として動く」という Erasable 原則に反して、ランタイムコードを生成するためです。
Node.js の type stripping (型注釈だけを取り除く実行方式。22.18 以降 / 24 ではフラグ不要でデフォルト有効) でも enum は ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX エラーになります。一方 Bun / Deno はフルトランスパイルで TypeScript を実行するため enum も動作しますが、erasableSyntaxOnly に合わせておけばどのランタイムでも同じコードがそのまま動きます。
新規コードでは as const オブジェクト + Union型リテラルを使うのが推奨です(後述「モダンなパターン」を参照)。本節の enum 解説は、既存コード読解のための知識として読んでください。
数値列挙型
// 基本的な数値列挙型(値は0から自動的に割り当てられる)
enum Direction {
North, // 0
South, // 1
East, // 2
West // 3
}
console.log(Direction.North); // 0
console.log(Direction.South); // 1
console.log(Direction.East); // 2
console.log(Direction.West); // 3
// 逆引き(数値から名前を取得)
console.log(Direction[0]); // 'North'
console.log(Direction[1]); // 'South'
初期値を指定
// 最初の値を指定すると、以降は自動でインクリメント
enum Status {
Pending = 1, // 1
InProgress, // 2
Completed, // 3
Cancelled // 4
}
// すべての値を明示的に指定
enum HttpStatus {
OK = 200,
Created = 201,
BadRequest = 400,
NotFound = 404,
InternalError = 500
}
console.log(HttpStatus.OK); // 200
console.log(HttpStatus.NotFound); // 404
console.log(HttpStatus[200]); // 'OK'
文字列列挙型
// 文字列列挙型(すべての値を明示的に指定する必要がある)
enum Color {
Red = 'RED',
Green = 'GREEN',
Blue = 'BLUE'
}
console.log(Color.Red); // 'RED'
console.log(Color.Green); // 'GREEN'
// 逆引きはできない(文字列列挙型の特徴)
// console.log(Color['RED']); // Error: 'RED' はメンバー名ではないため型エラー(JS 実行時なら undefined)
文字列列挙型の利点
// デバッグ時に意味のある値が表示される
enum LogLevel {
Debug = 'DEBUG',
Info = 'INFO',
Warning = 'WARNING',
Error = 'ERROR'
}
function log(level: LogLevel, message: string): void {
console.log(`[${level}] ${message}`);
}
log(LogLevel.Error, 'Something went wrong');
// 出力: [ERROR] Something went wrong
// 数値列挙型だと意味が分かりにくい
enum LogLevel2 {
Debug, // 0
Info, // 1
Warning, // 2
Error // 3
}
function log2(level: LogLevel2, message: string): void {
console.log(`[${level}] ${message}`);
}
log2(LogLevel2.Error, 'Something went wrong');
// 出力: [3] Something went wrong(3が何を意味するか分かりにくい)
列挙型の使用例
// 例1: アプリケーションの状態管理
enum AppState {
Initializing = 'INITIALIZING',
Ready = 'READY',
Running = 'RUNNING',
Paused = 'PAUSED',
Stopped = 'STOPPED'
}
let currentState: AppState = AppState.Initializing;
function setState(newState: AppState): void {
console.log(`State changed: ${currentState} → ${newState}`);
currentState = newState;
}
setState(AppState.Ready);
setState(AppState.Running);
// 例2: ユーザーの権限レベル
enum UserRole {
Guest = 'GUEST',
User = 'USER',
Admin = 'ADMIN',
SuperAdmin = 'SUPER_ADMIN'
}
function checkPermission(role: UserRole): boolean {
return role === UserRole.Admin || role === UserRole.SuperAdmin;
}
console.log(checkPermission(UserRole.User)); // false
console.log(checkPermission(UserRole.Admin)); // true
// 例3: 曜日
enum DayOfWeek {
Sunday,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday
}
function isWeekendDay(day: DayOfWeek): boolean {
return day === DayOfWeek.Sunday || day === DayOfWeek.Saturday;
}
console.log(isWeekendDay(DayOfWeek.Monday)); // false
console.log(isWeekendDay(DayOfWeek.Saturday)); // true
const enum(コンパイル時に最適化される列挙型)
// const enumを使用
const enum Direction {
North,
South,
East,
West
}
let dir: Direction = Direction.North;
console.log(dir); // 0
// コンパイル後のJavaScriptコード:
// let dir = 0 /* Direction.North */;
// console.log(dir);
const enumの特徴:
- コンパイル時に値がインライン化される
- 生成されるJavaScriptコードが小さくなる
- 逆引きはできない
const enum は isolatedModules: true と非互換
const enum は単一ファイル単位のコンパイル(isolatedModules: true)とは相性が悪く、Babel / esbuild / swc / Vite などのモダンなツールチェーンでは動作しません。以下の事情から、const enum は実質的に使えなくなっています。
--isolatedModules/verbatimModuleSyntaxと非互換--erasableSyntaxOnly(TS 5.8+)でもエラー- Library の
.d.tsに含まれるconst enumは利用側でインライン化できない
新規コードでは const enum を使わず、as const オブジェクトか通常の Union型リテラルを選んでください。
enumとUnion型の比較
// enumを使う場合
enum SizeEnum {
Small = 'SMALL',
Medium = 'MEDIUM',
Large = 'LARGE'
}
function getPriceEnum(size: SizeEnum): number {
switch (size) {
case SizeEnum.Small:
return 100;
case SizeEnum.Medium:
return 200;
case SizeEnum.Large:
return 300;
}
}
// Union型を使う場合
type SizeUnion = 'SMALL' | 'MEDIUM' | 'LARGE';
function getPriceUnion(size: SizeUnion): number {
switch (size) {
case 'SMALL':
return 100;
case 'MEDIUM':
return 200;
case 'LARGE':
return 300;
}
}
どちらを使うべきか
| 特徴 | enum | Union型 |
|---|---|---|
| 使いやすさ | Size.Smallのようにアクセス | 'SMALL'と直接文字列を使用 |
| コード量 | 定義が必要 | 型エイリアスだけで済む |
| 生成されるJS | オブジェクトが生成される | 何も生成されない(型情報のみ) |
| 逆引き | 数値enumのみ可能 | 不可能 |
| 推奨度 | 関連する定数のグループ化に適している | シンプルで軽量 |
モダンなTypeScriptでの推奨パターン
// Union型 + as constを使う方法(推奨)
const Size = {
Small: 'SMALL',
Medium: 'MEDIUM',
Large: 'LARGE'
} as const;
type Size = typeof Size[keyof typeof Size]; // 'SMALL' | 'MEDIUM' | 'LARGE'
// 使用例
let shirtSize: Size = Size.Small; // enum風に使える
shirtSize = 'MEDIUM'; // 文字列リテラルとしても使える
satisfies 演算子と組み合わせる(TS 4.9+)
satisfies を使うと、「オブジェクトが特定の型を満たしていることを検査」しつつ「推論されたリテラル型を維持」できます。型注釈(: Type)では具体型が失われるケースを、satisfies で補えます。
// ❌ 型注釈だと具体的な値の型情報が失われる
const sizeConfig1: Record<string, string> = {
Small: 'SMALL',
Medium: 'MEDIUM',
Large: 'LARGE',
} as const;
// sizeConfig1.Small は string 型になってしまう
// ✅ satisfies なら型チェックしつつリテラル型を保持
const sizeConfig2 = {
Small: 'SMALL',
Medium: 'MEDIUM',
Large: 'LARGE',
} as const satisfies Record<string, string>;
// sizeConfig2.Small は 'SMALL' 型のまま
// タイプセーフな lookup table の典型パターン
type Size = 'SMALL' | 'MEDIUM' | 'LARGE';
const sizePrice = {
SMALL: 100,
MEDIUM: 200,
LARGE: 300,
} as const satisfies Record<Size, number>;
// Size が増えたときに sizePrice のキー漏れがコンパイルエラーで検知できる
// かつ sizePrice.SMALL の型は 100(リテラル型)のまま保持される
このパターンは 2026年の TypeScript コードで頻出します。「値を定義 → as const で fix → satisfies で形をチェック」の 3 段構えとして覚えておくと使い回しやすいです。
試してみよう: ステータス管理を実装しよう ★★
以下の要件を満たすタスク管理システムの型とステータス変更関数を実装してください。
要件:
- タスクのステータスを定義(Union型またはenum)
- 'pending': 未着手
- 'in_progress': 進行中
- 'completed': 完了
- 'cancelled': キャンセル
getStatusMessage関数を作成(ステータスに応じた日本語メッセージを返す)canTransition関数を作成(ステータス遷移が可能かどうかを判定)- pending → in_progress: OK
- in_progress → completed: OK
- in_progress → cancelled: OK
- completed → any: NG(完了後は変更不可)
- cancelled → any: NG(キャンセル後は変更不可)
ヒント
- ステータスは
type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'cancelled';のように定義 getStatusMessageはswitch文で各ステータスのメッセージを返すcanTransitionは現在のステータスと次のステータスを引数に取り、booleanを返す- 完了・キャンセル済みの場合は早期リターンでfalseを返す
回答と解説
// ステータスの型定義(Union型を使用)
type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'cancelled';
// ステータスに応じた日本語メッセージを返す関数
function getStatusMessage(status: TaskStatus): string {
switch (status) {
case 'pending':
return 'タスクは未着手です';
case 'in_progress':
return 'タスクは進行中です';
case 'completed':
return 'タスクは完了しました';
case 'cancelled':
return 'タスクはキャンセルされました';
}
}
// ステータス遷移が可能かどうかを判定する関数
function canTransition(current: TaskStatus, next: TaskStatus): boolean {
// 完了またはキャンセル済みの場合は変更不可
if (current === 'completed' || current === 'cancelled') {
return false;
}
// 同じステータスへの遷移は不可
if (current === next) {
return false;
}
// pending からの遷移
if (current === 'pending') {
return next === 'in_progress' || next === 'cancelled';
}
// in_progress からの遷移
if (current === 'in_progress') {
return next === 'completed' || next === 'cancelled';
}
return false;
}
// テスト
console.log(getStatusMessage('pending')); // 'タスクは未着手です'
console.log(getStatusMessage('in_progress')); // 'タスクは進行中です'
console.log(getStatusMessage('completed')); // 'タスクは完了しました'
console.log(getStatusMessage('cancelled')); // 'タスクはキャンセルされました'
console.log(canTransition('pending', 'in_progress')); // true
console.log(canTransition('in_progress', 'completed')); // true
console.log(canTransition('in_progress', 'cancelled')); // true
console.log(canTransition('completed', 'pending')); // false
console.log(canTransition('cancelled', 'in_progress')); // false
console.log(canTransition('pending', 'completed')); // false(直接完了は不可)
解説:
- Union型を使うことで、許容されるステータスを厳密に定義しています
getStatusMessage関数のswitch文では、すべてのケースを網羅しているため、default句は不要ですcanTransition関数では、早期リターンパターンを使って、不正な遷移を最初に弾いています- TypeScriptの型チェックにより、存在しないステータス(例:
'unknown')は使用できません
enumを使った代替実装:
enum TaskStatus {
Pending = 'PENDING',
InProgress = 'IN_PROGRESS',
Completed = 'COMPLETED',
Cancelled = 'CANCELLED'
}
function getStatusMessage(status: TaskStatus): string {
switch (status) {
case TaskStatus.Pending:
return 'タスクは未着手です';
case TaskStatus.InProgress:
return 'タスクは進行中です';
case TaskStatus.Completed:
return 'タスクは完了しました';
case TaskStatus.Cancelled:
return 'タスクはキャンセルされました';
}
}
Union型とenumのどちらを使うかは、プロジェクトの方針やチームの好みによります。Union型はシンプルで軽量、enumは名前空間として使いやすいという特徴があります。
まとめ
この章で学んだこと:
- リテラル型: 特定の値のみを許可する型。as constで固定できる
- Union型: 複数の型のいずれかを許容する型(
A | B | C) - 型の絞り込み: typeof等を使ってUnion型から特定の型に絞り込む
- 列挙型(enum): 関連する定数をグループ化。数値enumと文字列enumがある
- モダンなパターン: Union型 + as constの組み合わせが推奨される場合も多い
重要なポイント:
- リテラル型とUnion型を組み合わせて、許容値を厳密に制限しよう
- switch文で網羅性チェックを行おう
- enumとUnion型の特徴を理解して、適切な方を選ぼう
次の章では、関数の基本について学びます。
初学者がつまずきやすいポイント
よくある間違い
❌ as constを付け忘れてリテラル型にならない
// ❌ 間違い: as constがないとstring型になる
const config = {
theme: 'dark',
language: 'ja'
};
// config.theme の型は string('dark'ではない)
function setTheme(theme: 'light' | 'dark'): void {
console.log(theme);
}
// setTheme(config.theme); // Error: string型は'light' | 'dark'に代入できない
// ✅ 正しい: as constでリテラル型に固定
const config2 = {
theme: 'dark',
language: 'ja'
} as const;
// config2.theme の型は 'dark'
setTheme(config2.theme); // OK
原因: オブジェクトのプロパティはデフォルトでは広い型(string, number等)に推論されます。
解決策: as constを使ってリテラル型として固定してください。
❌ Union型の変数をそのまま操作しようとする
// ❌ 間違い: 型の絞り込みをせずに操作
function processValue(value: string | number): string {
return value.toUpperCase(); // Error: numberにtoUpperCaseはない
}
// ✅ 正しい: 型ガードで絞り込んでから操作
function processValue(value: string | number): string {
if (typeof value === 'string') {
return value.toUpperCase();
}
return value.toString();
}
原因: Union型の変数には、各型の共通メソッドしか直接呼び出せません。
解決策: typeofやinstanceofで型を絞り込んでから操作してください。
❌ enumの数値を直接比較してしまう
enum Status {
Pending, // 0
Active, // 1
Completed // 2
}
// ❌ 間違い: マジックナンバーとの比較
function checkStatus(status: Status): void {
if (status === 1) { // 動作するが、可読性が低い
console.log('Active');
}
}
// ✅ 正しい: enum値との比較
function checkStatus(status: Status): void {
if (status === Status.Active) {
console.log('Active');
}
}
原因: 数値enumは数値との比較が可能ですが、意図が不明確になります。
解決策: 常にenum値を使って比較し、コードの意図を明確にしてください。
❌ 文字列enumで逆引きしようとする
enum Color {
Red = 'RED',
Green = 'GREEN',
Blue = 'BLUE'
}
// ❌ 間違い: 文字列enumは逆引きできない
console.log(Color['RED']); // Error: Property 'RED' does not exist('Red' というメンバー名ではなく値 'RED' でアクセスしているため型エラー。JS として実行した場合は undefined)
// 数値enumは逆引き可能
enum Direction {
North, // 0
South // 1
}
console.log(Direction[0]); // 'North'
原因: 文字列enumはコンパイル時に逆引き用のマッピングが生成されません。
解決策: 文字列enumでは逆引きを期待せず、必要ならオブジェクトを別途作成してください。
❌ Union型とenumを混同する
// enumを使う場合
enum SizeEnum {
Small = 'SMALL',
Medium = 'MEDIUM',
Large = 'LARGE'
}
// Union型を使う場合
type SizeUnion = 'SMALL' | 'MEDIUM' | 'LARGE';
// ❌ 間違い: enumとUnion型の値を混同
let size1: SizeEnum = 'SMALL'; // Error: 文字列は直接代入できない
let size2: SizeEnum = SizeEnum.Small; // OK
let size3: SizeUnion = 'SMALL'; // OK(Union型は直接代入可能)
原因: enumは独自の型であり、文字列リテラルと互換性がありません。
解決策: enumを使う場合はEnum.Value形式でアクセスしてください。