TypeScriptは日々進化を続けており、開発現場において型安全性を確保するための重要な手段となっています。

その中でも、複数の型を組み合わせることができる「ユニオン型(Union Types)」は、柔軟なプログラミングを実現するために欠かせない機能です。

コードの堅牢性を高めつつ、JavaScript特有の柔軟なデータ構造を安全に扱うためには、ユニオン型の深い理解が求められます。

本記事では、ユニオン型の基礎から、実戦で役立つ型ガードのテクニック、さらには2026年現在の開発標準に合わせたベストプラクティスまでを詳しく解説します。

ユニオン型の基本概念と構文

ユニオン型とは、「複数の型のうち、いずれか一つに該当すること」を表現するための機能です。

TypeScriptでは、パイプ記号 | を使用して複数の型を結合します。

これにより、一つの変数に対して異なる種類のデータが代入される可能性を、型定義として明示的に宣言できます。

複数の型を許容する柔軟性

例えば、関数の引数が数値と文字列の両方を受け取る必要がある場合、ユニオン型が非常に有効です。

以下のコード例では、IDとして数値と文字列の両方を受け入れる関数を定義しています。

TypeScript
// 数値、または文字列を受け取るユニオン型の定義
function printId(id: number | string) {
  console.log("Your ID is: " + id);
}

// どちらの型でも呼び出し可能
printId(101);
printId("202");
実行結果
Your ID is: 101
Your ID is: 202

このように定義することで、開発者はIDが数値であっても文字列であっても、TypeScriptのコンパイラから警告を受けることなく処理を記述できます。

JavaScriptの動的な柔軟性を維持しつつ、想定外の型が混入することを防ぐのがユニオン型の役割です。

リテラル型との組み合わせ

ユニオン型は、具体的な値そのものを型とする「リテラル型」と組み合わせることで、より真価を発揮します。

特定の文字列のみを許可する設定などは、モダンなTypeScript開発において非常によく使われるパターンです。

TypeScript
// 特定の文字列のみを許容するリテラル型のユニオン
type ButtonSize = "small" | "medium" | "large";

function createButton(size: ButtonSize) {
  // 実装内容
  console.log(`Creating a ${size} button.`);
}

// 許可された値はOK
createButton("medium");

// 許可されていない値を渡すとコンパイルエラー
// createButton("extra-large");

この手法を用いることで、マジックナンバーやスペルミスによるバグを未然に防ぐことができます。

「とりうる値を限定する」というアプローチは、コードの自己文書化にも繋がり、他の開発者がコードを読む際の手助けとなります。

オブジェクトにおけるユニオン型の活用

ユニオン型はプリミティブな値だけでなく、オブジェクトに対しても適用可能です。

しかし、単純に複数のオブジェクト型を | で繋ぐだけでは、プロパティへのアクセス時に制限が生じることがあります。

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

複雑なデータ構造を安全に扱うための最も強力な手法が、「判別可能なユニオン型」です。

これは、各オブジェクト型に共通の「リテラル型プロパティ」を持たせ、それを目印にして型を特定する手法です。

TypeScript
interface Circle {
  kind: "circle"; // 判別用のプロパティ
  radius: number;
}

interface Square {
  kind: "square"; // 判別用のプロパティ
  sideLength: number;
}

type Shape = Circle | Square;

function getArea(shape: Shape) {
  // shape.radius にアクセスしようとすると、Squareの可能性を指摘されエラーになる
  
  if (shape.kind === "circle") {
    // ここでは shape は Circle 型として扱われる
    return Math.PI * shape.radius ** 2;
  } else {
    // ここでは shape は Square 型として扱われる
    return shape.sideLength ** 2;
  }
}

上記の例では、kind というプロパティが判別のためのフラグとして機能しています。

TypeScriptのコンパイラは、if 文や switch 文による条件分岐を解析し、そのブロック内での型を自動的に絞り込んでくれます。

この仕組みにより、安全性が保証された状態でプロパティにアクセスすることが可能になります。

型ガード(Type Guards)による安全な処理

ユニオン型を使用する場合、その変数が「具体的にどの型であるか」を判定するプロセスが必要です。

これを「型ガード」と呼びます。

型ガードを適切に使用することで、ユニオン型のポテンシャルを最大限に引き出すことができます。

typeof演算子を用いた絞り込み

最も基本的な型ガードは、JavaScript標準の typeof 演算子を使用する方法です。

プリミティブ型(string, number, booleanなど)のユニオン型を扱う場合に適しています。

TypeScript
function formatInput(input: string | number) {
  if (typeof input === "string") {
    // input は string 型に絞り込まれる
    return input.trim().toUpperCase();
  }
  // ここでは input は number 型として扱われる
  return input.toFixed(2);
}

このように、条件分岐によって型を確定させることで、その型特有のメソッドを安全に呼び出せるようになります。

in演算子を用いたプロパティ確認

オブジェクトが特定のプロパティを持っているかどうかで判定する場合は、in 演算子が便利です。

TypeScript
interface Admin {
  name: string;
  privileges: string[];
}

interface Employee {
  name: string;
  startDate: Date;
}

type UnknownUser = Admin | Employee;

function printInformation(user: UnknownUser) {
  console.log("Name: " + user.name);
  
  if ("privileges" in user) {
    // user は Admin 型に絞り込まれる
    console.log("Privileges: " + user.privileges.join(", "));
  }
  
  if ("startDate" in user) {
    // user は Employee 型に絞り込まれる
    console.log("Start Date: " + user.startDate);
  }
}

in 演算子は、特定のプロパティが存在するかどうかをチェックし、それに基づいて型を絞り込みます。

共通のプロパティ(この例では name)には分岐なしでアクセスでき、固有のプロパティには安全なガードを設けることができます。

instanceofを用いたクラス判定

クラスのインスタンスを扱う場合は、instanceof を使用して型ガードを行います。

TypeScript
class FileLogger {
  logToFile(message: string) {
    console.log("Writing to file: " + message);
  }
}

class ConsoleLogger {
  logToConsole(message: string) {
    console.log("Console output: " + message);
  }
}

type Logger = FileLogger | ConsoleLogger;

function executeLog(logger: Logger) {
  if (logger instanceof FileLogger) {
    logger.logToFile("Process started");
  } else {
    logger.logToConsole("Process started");
  }
}

クラスベースのオブジェクト指向プログラミングをTypeScriptで行う際には、この手法が不可欠です。

ユーザー定義の型ガード

より複雑な判定ロジックが必要な場合は、ユーザー定義の型ガードを作成します。

戻り値の型として parameterName is Type という形式(型述語)を使用するのが特徴です。

TypeScript
interface Bird {
  fly(): void;
  layEggs(): void;
}

interface Fish {
  swim(): void;
  layEggs(): void;
}

// ユーザー定義の型ガード
function isFish(pet: Bird | Fish): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

function movePet(pet: Bird | Fish) {
  if (isFish(pet)) {
    pet.swim();
  } else {
    pet.fly();
  }
}

この手法を使うと、判定ロジックを関数として切り出すことができ、コードの再利用性と可読性が向上します。

実践的なユースケース:APIレスポンスの処理

実際のアプリケーション開発において、ユニオン型が最も威力を発揮するのはAPI通信の結果を扱う場面です。

成功時のデータと失敗時のエラー情報を一つのユニオン型で定義することで、エラーハンドリングを強制できます。

TypeScript
type ApiResponse<T> = 
  | { status: "success"; data: T }
  | { status: "error"; message: string; code: number };

async function fetchData(): Promise<ApiResponse<{ id: number; name: string }>> {
  try {
    // 通信処理のシミュレーション
    const response = { id: 1, name: "Sample Data" };
    return { status: "success", data: response };
  } catch (e) {
    return { status: "error", message: "Failed to fetch", code: 500 };
  }
}

async function main() {
  const result = await fetchData();
  
  if (result.status === "success") {
    // 成功時のみ data プロパティにアクセス可能
    console.log(result.data.name);
  } else {
    // 失敗時は message と code を使用
    console.error(`Error ${result.code}: ${result.message}`);
  }
}

このように定義しておけば、「成功したかどうかをチェックせずにデータにアクセスする」という実装ミスをコンパイル段階で防ぐことができます。

「状態」を型で表現することは、堅牢なフロントエンド開発において非常に重要なテクニックです。

ユニオン型を扱う際の注意点とベストプラクティス

ユニオン型は強力ですが、使いすぎるとコードが複雑になり、かえってメンテナンス性を低下させることもあります。

共通プロパティへのアクセス

ユニオン型の変数に対して、型ガードなしでアクセスできるのは、構成するすべての型に含まれているプロパティのみです。

共通でないプロパティにアクセスしようとすると、TypeScriptはエラーを発生させます。

共通プロパティをうまく定義することで、ガードの回数を減らし、コードを簡潔に保つことができます。

複雑すぎるユニオン型の回避

一つの変数が「10種類もの異なる型」を持つような設計は避けるべきです。

あまりにも多くの型を結合すると、条件分岐が肥大化し、ロジックの見通しが悪くなります。

そのような場合は、共通のインターフェースを抽出したり、型定義を分割したりすることを検討してください。

never型を用いた網羅性チェック

ユニオン型のすべてのパターンを switch 文などで処理しているかを確認するために、never 型を活用する方法があります。

TypeScript
type Color = "red" | "blue" | "green";

function getColorName(color: Color) {
  switch (color) {
    case "red":
      return "赤";
    case "blue":
      return "青";
    case "green":
      return "緑";
    default:
      // すべてのケースが網羅されていれば、ここには到達しない
      const _exhaustiveCheck: never = color;
      return _exhaustiveCheck;
  }
}

もし将来的に Color 型に "yellow" が追加された場合、default ブロックでコンパイルエラーが発生します。

これにより、新しく追加された型に対する処理の書き忘れを確実に防ぐことができます。

まとめ

TypeScriptのユニオン型は、柔軟性と安全性を両立させるための鍵となる機能です。

単なる「複数の型を許可する仕組み」としてだけでなく、リテラル型や判別可能なユニオン型と組み合わせることで、アプリケーションの状態を正確にモデル化できます。

また、typeofin、ユーザー定義の型ガードを使いこなすことで、型安全なコードを維持したまま、複雑なデータ構造をスマートに処理できるようになります。

まずは小さなところから、リテラル型のユニオンやシンプルな型ガードを取り入れてみてください。

本記事で紹介したテクニックを実践することで、バグの少ない、メンテナンス性の高いTypeScriptコードを記述できるはずです。

2026年の開発トレンドにおいても、こうした静的解析を活かした設計思想はますます重要性を増していくでしょう。