メインコンテンツまでスキップ

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の効果

  1. すべてのプロパティがreadonlyになる
  2. プリミティブ値はリテラル型になる
  3. 配列は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 ではフラグ不要でデフォルト有効) でも enumERR_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 enumisolatedModules: 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;
}
}

どちらを使うべきか

特徴enumUnion型
使いやすさ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 段構えとして覚えておくと使い回しやすいです。

試してみよう: ステータス管理を実装しよう ★★

以下の要件を満たすタスク管理システムの型とステータス変更関数を実装してください。

要件:

  1. タスクのステータスを定義(Union型またはenum)
    • 'pending': 未着手
    • 'in_progress': 進行中
    • 'completed': 完了
    • 'cancelled': キャンセル
  2. getStatusMessage関数を作成(ステータスに応じた日本語メッセージを返す)
  3. canTransition関数を作成(ステータス遷移が可能かどうかを判定)
    • pending → in_progress: OK
    • in_progress → completed: OK
    • in_progress → cancelled: OK
    • completed → any: NG(完了後は変更不可)
    • cancelled → any: NG(キャンセル後は変更不可)
ヒント
  1. ステータスはtype TaskStatus = 'pending' | 'in_progress' | 'completed' | 'cancelled';のように定義
  2. getStatusMessageはswitch文で各ステータスのメッセージを返す
  3. canTransitionは現在のステータスと次のステータスを引数に取り、booleanを返す
  4. 完了・キャンセル済みの場合は早期リターンで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型の変数には、各型の共通メソッドしか直接呼び出せません。

解決策: typeofinstanceofで型を絞り込んでから操作してください。

❌ 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形式でアクセスしてください。


次に読む