はじめに

TypeScriptで開発を進めていると、「この変数には文字列か数値のどちらかが入る」「このパラメータは特定の文字列のいずれかしか受け付けない」といった場面に遭遇します。このような「複数の型のいずれかを許容する」状況を型安全に扱うのが、TypeScriptのユニオン型です。

本記事では、TypeScriptのユニオン型(Union Types)の基本から、リテラル型との組み合わせ、型の絞り込み(Narrowing)、そして判別可能なユニオン型(Discriminated Unions)まで、段階的に解説します。

この記事を読み終えると、以下のことができるようになります。

  • ユニオン型を使って複数の型を許容する変数や関数を定義できる
  • リテラル型で特定の値のみを許容する型を作成できる
  • 型ガードを使ってユニオン型を安全に絞り込める
  • Discriminated Unionsパターンで複雑な型を扱える

実行環境・前提条件

前提知識

動作確認環境

ツール バージョン
Node.js 20.x以上
TypeScript 5.7以上
VS Code 最新版

本記事のサンプルコードは、TypeScript Playgroundで動作確認できます。ローカル環境で実行する場合は、開発環境構築ガイドを参照してください。

期待される結果

本記事のコードを実行すると、以下の動作を確認できます。

  • ユニオン型を持つ変数に複数の型の値を代入できる
  • 型ガードによって適切な型に絞り込まれた状態でメソッドを呼び出せる
  • Discriminated Unionsで網羅性チェックが機能する

TypeScriptユニオン型の基本

ユニオン型は、2つ以上の型を|(パイプ)で結合して定義します。ユニオン型の変数には、結合された型のいずれかの値を代入できます。

graph TB
    A[ユニオン型] --> B["string | number"]
    B --> C["'hello' - string型"]
    B --> D["42 - number型"]
    B --> E["true - コンパイルエラー"]
    style E fill:#ffcccc

ユニオン型の定義

ユニオン型を定義する基本的な構文は以下のとおりです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// ユニオン型の変数定義
let id: string | number;

// string型の値を代入
id = "user-001";
console.log(id); // "user-001"

// number型の値を代入
id = 12345;
console.log(id); // 12345

// boolean型はコンパイルエラー
// id = true; // Error: Type 'boolean' is not assignable to type 'string | number'

関数パラメータでのユニオン型

関数の引数にユニオン型を使うことで、複数の型を受け入れる柔軟な関数を定義できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function printId(id: number | string): void {
  console.log(`Your ID is: ${id}`);
}

// number型で呼び出し
printId(101);     // "Your ID is: 101"

// string型で呼び出し
printId("202");   // "Your ID is: 202"

// オブジェクト型はエラー
// printId({ myId: 22342 }); // Error

ユニオン型の共通操作

ユニオン型の値に対してメソッドを呼び出す場合、TypeScriptはすべてのメンバー型で有効な操作のみを許可します。

1
2
3
4
5
6
7
function printId(id: number | string): void {
  // toUpperCase()はstring型にしか存在しないためエラー
  // console.log(id.toUpperCase()); // Error

  // toString()はnumberとstring両方に存在するためOK
  console.log(id.toString());
}

この制約があるからこそ、TypeScriptは型安全性を保証できます。では、特定の型でしか使えないメソッドを呼び出すにはどうすればよいでしょうか。その答えが「型の絞り込み(Narrowing)」です。

TypeScriptリテラル型とは

リテラル型は、特定の値そのものを型として扱う機能です。プリミティブ型(string、number、boolean)を、より具体的な値に限定できます。

文字列リテラル型

文字列リテラル型を使うと、特定の文字列のみを許容する型を定義できます。

1
2
3
4
5
6
7
8
// 文字列リテラル型
let direction: "left" | "right" | "center";

direction = "left";   // OK
direction = "right";  // OK
direction = "center"; // OK

// direction = "top"; // Error: Type '"top"' is not assignable to type '"left" | "right" | "center"'

関数の引数に文字列リテラル型を使うと、許容される値をコンパイル時にチェックできます。

1
2
3
4
5
6
7
8
function setAlignment(alignment: "left" | "right" | "center"): void {
  console.log(`Alignment set to: ${alignment}`);
}

setAlignment("left");   // OK
setAlignment("right");  // OK

// setAlignment("top"); // Error: Argument of type '"top"' is not assignable

数値リテラル型

数値リテラル型も同様に、特定の数値のみを許容する型を定義できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 数値リテラル型
type DiceValue = 1 | 2 | 3 | 4 | 5 | 6;

function rollDice(): DiceValue {
  return (Math.floor(Math.random() * 6) + 1) as DiceValue;
}

const result: DiceValue = rollDice();
console.log(`Rolled: ${result}`);

// 比較関数の戻り値型
function compare(a: string, b: string): -1 | 0 | 1 {
  return a === b ? 0 : a > b ? 1 : -1;
}

booleanリテラル型

boolean型は、実はtrue | falseのユニオン型のエイリアスです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// boolean は true | false のエイリアス
type MyBoolean = true | false;

// 特定のboolean値のみを許容する関数
function setEnabled(flag: true): void {
  console.log("Feature enabled");
}

setEnabled(true);  // OK
// setEnabled(false); // Error

リテラル型とオブジェクトの組み合わせ

リテラル型は、オブジェクト型と組み合わせることで、より表現力豊かな型を定義できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
interface Config {
  width: number;
}

function configure(options: Config | "auto"): void {
  if (options === "auto") {
    console.log("Auto configuration");
  } else {
    console.log(`Width: ${options.width}`);
  }
}

configure({ width: 100 }); // "Width: 100"
configure("auto");         // "Auto configuration"

constアサーションとリテラル推論

オブジェクトリテラルを変数に代入すると、プロパティの型は通常より広い型(stringなど)に推論されます。as constを使うと、リテラル型として推論させることができます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 通常の推論: method は string 型
const req1 = { url: "https://example.com", method: "GET" };
// req1.method の型は string

// as const を使用: method は "GET" リテラル型
const req2 = { url: "https://example.com", method: "GET" } as const;
// req2.method の型は "GET"

// HTTPメソッドを受け取る関数
declare function makeRequest(url: string, method: "GET" | "POST"): void;

// req1.method は string 型なのでエラー
// makeRequest(req1.url, req1.method); // Error

// req2.method は "GET" リテラル型なのでOK
makeRequest(req2.url, req2.method); // OK

TypeScript型の絞り込み(Narrowing)

ユニオン型の変数に対して、条件分岐を使って型を絞り込む技術をNarrowingと呼びます。TypeScriptは制御フロー解析を行い、各分岐で変数がどの型であるかを追跡します。

flowchart TD
    A["id: string | number"] --> B{typeof id === 'string'?}
    B -->|true| C["id: string"]
    B -->|false| D["id: number"]
    C --> E["id.toUpperCase() - OK"]
    D --> F["id.toFixed() - OK"]

typeof型ガード

typeof演算子を使って、プリミティブ型を判定できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function printId(id: number | string): void {
  if (typeof id === "string") {
    // この分岐内では id は string 型
    console.log(id.toUpperCase());
  } else {
    // この分岐内では id は number 型
    console.log(id.toFixed(2));
  }
}

printId("hello");  // "HELLO"
printId(42.5);     // "42.50"

typeofが返す文字列は以下のとおりです。

値の種類 typeof の結果
string “string”
number “number”
bigint “bigint”
boolean “boolean”
symbol “symbol”
undefined “undefined”
null “object”
関数 “function”
その他のオブジェクト “object”

注意点として、typeof null"object"を返します。これはJavaScriptの歴史的な仕様です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function printAll(strs: string | string[] | null): void {
  if (strs && typeof strs === "object") {
    // strs は string[] 型(null は除外済み)
    for (const s of strs) {
      console.log(s);
    }
  } else if (typeof strs === "string") {
    console.log(strs);
  }
}

等価性による絞り込み

===!====!=を使った等価性チェックでも型を絞り込めます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function example(x: string | number, y: string | boolean): void {
  if (x === y) {
    // x と y が等しい場合、共通の型は string のみ
    console.log(x.toUpperCase()); // x: string
    console.log(y.toLowerCase()); // y: string
  } else {
    console.log(x); // x: string | number
    console.log(y); // y: string | boolean
  }
}

nullチェックでは、== nullnullundefinedの両方をチェックできることを活用できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
interface Container {
  value: number | null | undefined;
}

function multiplyValue(container: Container, factor: number): void {
  // != null は null と undefined の両方を除外
  if (container.value != null) {
    console.log(container.value * factor); // value: number
  }
}

in演算子による絞り込み

in演算子を使って、オブジェクトに特定のプロパティが存在するかをチェックできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
type Fish = { swim: () => void };
type Bird = { fly: () => void };

function move(animal: Fish | Bird): void {
  if ("swim" in animal) {
    // animal は Fish 型
    animal.swim();
  } else {
    // animal は Bird 型
    animal.fly();
  }
}

instanceof演算子による絞り込み

instanceof演算子を使って、オブジェクトがクラスのインスタンスかどうかをチェックできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function logValue(x: Date | string): void {
  if (x instanceof Date) {
    // x は Date 型
    console.log(x.toUTCString());
  } else {
    // x は string 型
    console.log(x.toUpperCase());
  }
}

logValue(new Date()); // "Fri, 03 Jan 2026 12:00:00 GMT"
logValue("hello");    // "HELLO"

真偽値による絞り込み

JavaScriptの真偽値評価(Truthiness)を利用した絞り込みも可能です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function printName(name: string | null | undefined): void {
  if (name) {
    // name は string 型(null, undefined, 空文字列を除外)
    console.log(`Hello, ${name.toUpperCase()}`);
  } else {
    console.log("No name provided");
  }
}

printName("Alice");     // "Hello, ALICE"
printName(null);        // "No name provided"
printName(undefined);   // "No name provided"
printName("");          // "No name provided" (空文字列もfalsy)

空文字列もfalsyな値として扱われる点に注意が必要です。空文字列を有効な値として扱いたい場合は、明示的に!= nullでチェックしましょう。

ユーザー定義型ガード

より複雑な型チェックには、ユーザー定義型ガード(Type Predicate)を使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
type Fish = { name: string; swim: () => void };
type Bird = { name: string; fly: () => void };

// 型述語 (pet is Fish) を返す関数
function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

function getSmallPet(): Fish | Bird {
  // 実装省略
  return { name: "Nemo", swim: () => console.log("Swimming!") };
}

const pet = getSmallPet();

if (isFish(pet)) {
  // pet は Fish 型
  pet.swim();
} else {
  // pet は Bird 型
  pet.fly();
}

配列のフィルタリングにも型ガードを活用できます。

1
2
3
4
const zoo: (Fish | Bird)[] = [getSmallPet(), getSmallPet(), getSmallPet()];

// isFish でフィルタリングすると Fish[] 型になる
const fishOnly: Fish[] = zoo.filter(isFish);

TypeScript判別可能なユニオン型(Discriminated Unions)

Discriminated Unions(判別可能なユニオン型)は、共通の判別プロパティ(discriminant)を持つ型のユニオンです。このパターンを使うと、複雑な型を安全に扱えます。

graph TB
    A[Shape] --> B[Circle]
    A --> C[Square]
    A --> D[Triangle]
    B --> E["kind: 'circle'<br/>radius: number"]
    C --> F["kind: 'square'<br/>sideLength: number"]
    D --> G["kind: 'triangle'<br/>base: number<br/>height: number"]

問題のあるアプローチ

まず、Discriminated Unionsを使わない場合の問題点を見てみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 問題のあるアプローチ
interface Shape {
  kind: "circle" | "square";
  radius?: number;      // circle のみで使用
  sideLength?: number;  // square のみで使用
}

function getArea(shape: Shape): number {
  if (shape.kind === "circle") {
    // radius が undefined の可能性があるためエラー
    // return Math.PI * shape.radius ** 2; // Error
    return Math.PI * shape.radius! ** 2; // 非nullアサーションが必要
  } else {
    return shape.sideLength! ** 2;
  }
}

このアプローチでは、!(非nullアサーション)を使わざるを得ず、型安全性が損なわれます。

Discriminated Unionsによる解決

各図形を別々のインターフェースとして定義し、それらのユニオン型を作成します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
interface Circle {
  kind: "circle";  // 判別プロパティ(リテラル型)
  radius: number;
}

interface Square {
  kind: "square";  // 判別プロパティ(リテラル型)
  sideLength: number;
}

interface Triangle {
  kind: "triangle";  // 判別プロパティ(リテラル型)
  base: number;
  height: number;
}

// 判別可能なユニオン型
type Shape = Circle | Square | Triangle;

判別プロパティによる絞り込み

kindプロパティをチェックすることで、TypeScriptは自動的に型を絞り込みます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      // shape は Circle 型に絞り込まれる
      return Math.PI * shape.radius ** 2;
    case "square":
      // shape は Square 型に絞り込まれる
      return shape.sideLength ** 2;
    case "triangle":
      // shape は Triangle 型に絞り込まれる
      return (shape.base * shape.height) / 2;
  }
}

const circle: Circle = { kind: "circle", radius: 5 };
const square: Square = { kind: "square", sideLength: 10 };
const triangle: Triangle = { kind: "triangle", base: 8, height: 6 };

console.log(getArea(circle));    // 78.53981633974483
console.log(getArea(square));    // 100
console.log(getArea(triangle));  // 24

網羅性チェック(Exhaustiveness Checking)

Discriminated Unionsの強力な特徴として、never型を使った網羅性チェックがあります。すべてのケースを処理しないとコンパイルエラーになります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    case "triangle":
      return (shape.base * shape.height) / 2;
    default:
      // すべてのケースを処理していれば、ここには到達しない
      // shape は never 型になる
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck;
  }
}

新しい図形を追加した場合を見てみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interface Rectangle {
  kind: "rectangle";
  width: number;
  height: number;
}

// Rectangle を追加
type Shape = Circle | Square | Triangle | Rectangle;

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    case "triangle":
      return (shape.base * shape.height) / 2;
    // case "rectangle" を追加し忘れると...
    default:
      // Error: Type 'Rectangle' is not assignable to type 'never'
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck;
  }
}

case "rectangle"を追加し忘れると、default節でshapeRectangle型となり、never型に代入できないためコンパイルエラーになります。

実践的な活用例:APIレスポンス

Discriminated Unionsは、APIレスポンスの型定義に最適です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// API レスポンスの型定義
interface SuccessResponse {
  status: "success";
  data: {
    id: number;
    name: string;
  };
}

interface ErrorResponse {
  status: "error";
  error: {
    code: number;
    message: string;
  };
}

interface LoadingResponse {
  status: "loading";
}

type ApiResponse = SuccessResponse | ErrorResponse | LoadingResponse;

function handleResponse(response: ApiResponse): void {
  switch (response.status) {
    case "success":
      console.log(`User: ${response.data.name}`);
      break;
    case "error":
      console.error(`Error ${response.error.code}: ${response.error.message}`);
      break;
    case "loading":
      console.log("Loading...");
      break;
  }
}

// 使用例
const success: SuccessResponse = {
  status: "success",
  data: { id: 1, name: "Alice" }
};

const error: ErrorResponse = {
  status: "error",
  error: { code: 404, message: "Not Found" }
};

handleResponse(success); // "User: Alice"
handleResponse(error);   // "Error 404: Not Found"

実践的な活用例:Redux風アクション

状態管理ライブラリのアクション定義にもDiscriminated Unionsが活用されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// アクションの型定義
interface IncrementAction {
  type: "INCREMENT";
}

interface DecrementAction {
  type: "DECREMENT";
}

interface SetValueAction {
  type: "SET_VALUE";
  payload: number;
}

type CounterAction = IncrementAction | DecrementAction | SetValueAction;

// リデューサー関数
function counterReducer(state: number, action: CounterAction): number {
  switch (action.type) {
    case "INCREMENT":
      return state + 1;
    case "DECREMENT":
      return state - 1;
    case "SET_VALUE":
      return action.payload;
  }
}

// 使用例
let count = 0;
count = counterReducer(count, { type: "INCREMENT" });       // 1
count = counterReducer(count, { type: "INCREMENT" });       // 2
count = counterReducer(count, { type: "SET_VALUE", payload: 10 }); // 10
count = counterReducer(count, { type: "DECREMENT" });       // 9

console.log(count); // 9

ユニオン型のベストプラクティス

型エイリアスで可読性を向上させる

複雑なユニオン型は、型エイリアスで名前を付けて可読性を向上させましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 悪い例:インラインで定義
function process(value: string | number | boolean | null | undefined): void {
  // ...
}

// 良い例:型エイリアスで定義
type Nullable<T> = T | null | undefined;
type PrimitiveValue = string | number | boolean;

function process(value: Nullable<PrimitiveValue>): void {
  // ...
}

判別プロパティは必ずリテラル型にする

Discriminated Unionsの判別プロパティは、必ずリテラル型で定義しましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 悪い例:判別プロパティが string 型
interface BadCircle {
  kind: string; // リテラル型ではない
  radius: number;
}

// 良い例:判別プロパティがリテラル型
interface GoodCircle {
  kind: "circle"; // リテラル型
  radius: number;
}

網羅性チェックを活用する

switch文では必ずdefault節で網羅性チェックを行い、将来の変更に備えましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function assertNever(x: never): never {
  throw new Error(`Unexpected value: ${x}`);
}

function handleShape(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    case "triangle":
      return (shape.base * shape.height) / 2;
    default:
      return assertNever(shape);
  }
}

まとめ

TypeScriptのユニオン型は、複数の型を持ちうる値を型安全に扱うための強力な機能です。本記事で解説した内容をまとめます。

機能 説明 主な用途
ユニオン型 A | Bで複数の型を結合 複数の型を許容する変数・引数
リテラル型 特定の値を型として扱う 許容する値の制限
型の絞り込み 条件分岐で型を特定 ユニオン型の安全な操作
Discriminated Unions 判別プロパティを持つユニオン 複雑な型の分岐処理

ユニオン型とNarrowingを理解することで、JavaScriptの柔軟性を維持しながら、TypeScriptの型安全性を最大限に活用できます。次のステップとして、型ガードのより詳細な使い方や、ジェネリクスとの組み合わせを学んでいくことをおすすめします。

参考リンク