7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Branded Typesを使用して型安全性を高める

Last updated at Posted at 2025-08-19

はじめに

TypeScriptで開発していると、関数の引数の順番を間違えてしまい、意図しないバグを生んでしまった経験はないでしょうか?

例えば、以下のコードを見てください。userIdaccountIdはどちらもstring型のため、コンパイラは引数の間違いを検知してくれません。

function transferMoney(fromId: string, toId: string, amount: number) {
  // 実装...
}

// 呼び出し時
const userId = "user123";
const accountId = "acc456";

// 意図しない使われ方だが、エラーにならない
transferMoney(accountId, userId, 1000);
// ↑ 引数の順番を間違えてもコンパイルエラーにならない!

このような「同じ型だが意味が異なる値」を区別できない問題を解決し、コードの型安全性を飛躍的に向上させるテクニックが Branded Types です。この記事では、Branded Typesの基本から実践的な使い方までを、解説します。


TypeScriptの構造的型付けとその課題

Branded Typesを理解するために、まずはTypeScriptの型システムである 構造的型付け(Structural Typing) についておさらいしましょう。

構造的型付けは、「型の構造が一致していれば、名前が違っても互換性がある」とみなすシステムで、TypeScriptの他にGoなどがこれに分類されます。

これに対し、名前的型付け(Nominal Typing)は、「型の名前が同じでなければ互換性がない」とみなすシステムで、Java, C#, C++, Swift, Rustなどが採用しています。

interface User {
  id: string;
  name: string;
}

interface Product {
  id: string;
  name: string;
}

function processUser(user: User) {
  console.log(user.name);
}

const product: Product = { id: "p1", name: "商品A" };
processUser(product); // エラーにならない!

UserProductは名前が違いますが、プロパティの構造が同じなので、TypeScriptはこれらを互換性のある型として扱います。これは便利な反面、冒頭の例のように、意図しない型を受け入れてしまうという課題も抱えています。


Branded Typesとは?

Branded Typesは、TypeScriptに名前的型付けの考え方を導入するための手法です。

具体的には、プリミティブ型に__brandというユニークなプロパティを交差型(&)で付与することで、型に「ブランド」を付けます。これにより、ベースの型が同じでも、ブランドが異ればTypeScriptが別の型として認識してくれるようになります。

// 構造的型付けの問題
type UserId = string;
type AccountId = string;
// UserId と AccountId は互換性がある(ただの別名)

// Branded Types で解決
type UserId = string & { readonly __brand: unique symbol };
type AccountId = string & { readonly __brand: unique symbol };
// これで UserId と AccountId は互換性がなくなる!

このテクニックの利点は、同じプリミティブ型(stringnumber)であっても、意味的に区別できるようになることです。

2つの実装パターン

Branded Typesにはいくつかの実装パターンがありますが本記事では2つの実装パターンを紹介します。どちらも実用上は同じように機能します。

パターン 1: unique symbol(最も厳密)

unique symbol を使うことで、他のコードとのブランド名の衝突を完全に避けることができます。

type UserId = string & { readonly __brand: unique symbol };
type ProductId = string & { readonly __brand: unique symbol };

パターン 2: リテラル型(実用的)

文字列リテラルをブランドとして使う方法で、シンプルで理解しやすいのが特徴です。ジェネリック型を使うと便利に定義できます。

type Brand<K, T> = K & { __brand: T };
type UserId = Brand<string, "UserId">;
type ProductId = Brand<string, "ProductId">;

実践例

例1: バリデーション済みかどうかの判断

ユーザー入力のように、検証が必要な値を型で表現する例を見てみましょう。

❌ Branded Typesを使わない場合

バリデーション済みかどうかが型で区別できないため、未検証のデータもデータベースに保存できてしまいます。

function validateUserInput(raw: string): string {
  // ...
  return sanitized;
}

function saveToDatabase(input: string) {
  db.save(input); // 未検証データも保存できてしまう
}

function handleSubmit() {
  const rawInput = userInput.value;

  // 危険!バリデーションを忘れても型エラーにならない
  saveToDatabase(rawInput); // コンパイルエラーなし 😱
}

✅ Branded Typesを使った場合

「検証済み」という状態を型で表現することで、バリデーションのし忘れをコンパイル時に防ぐことができます。

// バリデーション済みデータのみBranded Types
type ValidatedUserInput = string & { __brand: "ValidatedUserInput" };

function validateUserInput(raw: string): ValidatedUserInput {
  // ...
  return sanitized as ValidatedUserInput;
}

// バリデーション済みデータのみを受け付ける
function saveToDatabase(input: ValidatedUserInput) {
  db.save(input); // 型で安全性が保証される
}

function handleSubmit() {
  const rawInput = userInput.value; // 普通のstring型

  // saveToDatabase(rawInput); // 型エラー!未検証データは保存できない ✋

  const validated = validateUserInput(rawInput);
  saveToDatabase(validated); // OK!検証済みなので安全 ✅
}

例2: ドメインモデルでの活用

UserIdProductIdのように、ドメインにおける様々なIDをBranded Typesで区別することで、より安全なコードになります。

type Brand<K, T> = K & { __brand: T };
type UserId = Brand<string, "UserId">;
type ProductId = Brand<string, "ProductId">;
type OrderId = Brand<string, "OrderId">;

function getUser(id: UserId): User { /* ... */ }
function getProduct(id: ProductId): Product { /* ... */ }

function createOrder(userId: UserId, productId: ProductId): OrderId {
  // userId と productId の型が保証されている
  // ...
}

// 使用例
const userId = "user_123" as UserId;
const productId = "prod_456" as ProductId;

const order = createOrder(userId, productId); // OK
// const order2 = createOrder(productId, userId); // 型エラー!引数の順番が違う

例3: コンストラクタ関数を使った改善

"値" as UserIdのような型アサーションをコードのあちこちに書くのは、あまり美しくありません。
そこで、型アサーションを隠蔽するコンストラクタ(ファクトリ)関数を用意することで、より安全に値を作成できます。

// 型アサーションを隠蔽するコンストラクタ
const UserId = {
  create: (value: string): UserId => {
    if (!value) throw new Error("UserId cannot be empty");
    return value as UserId;
  },
};

const ProductId = {
  create: (value: string): ProductId => {
    if (!value) throw new Error("ProductId cannot be empty");
    return value as ProductId;
  },
};

// より安全な使用例
const userId = UserId.create("user123");
const productId = ProductId.create("prod456");

// 直接型アサーションする必要がない
// const badUserId = "user123" as UserId; // これを避けられる
const goodUserId = UserId.create("user123"); // こちらを推奨

パフォーマンスへの影響

Branded Typesはコンパイル時にのみ機能し、型情報はコンパイル後のJavaScriptコードからはすべて消え去ります。

// TypeScriptで書いたコード
type UserId = string & { __brand: "UserId" };
const userId: UserId = "user123" as UserId;
// ↓ コンパイル後のJavaScript
const userId = "user123"; // 型情報は全て消える!

結果として、ブラウザで動くJavaScriptは普通のstringと全く同じです。

  • ランタイムへの影響: メモリ使用量、実行速度、ファイルサイズ、すべてにおいて影響はありません
  • コンパイル時への影響: わずかな型チェック時間の増加はありますが、実用上問題になるレベルではありません。

導入時の注意点

Branded Typesを導入する際のメリットとデメリットを整理します。

メリット ✅

  • バグの早期発見: コンパイル時に型の不一致を検出できます。
  • APIの意図が明確: (id: UserId)のように、関数シグネチャが自己文書化されます。
  • リファクタリング時の安全性: 型の変更による影響範囲が明確になります。

デメリット ⚠️

  • 学習コスト: チームメンバーがこの概念を理解する必要があります。
  • 型アサーション: 値を生成する際にasが必要になります(コンストラクタ関数で隠蔽推奨)。
  • 既存コードへの影響: 既存のコードベースに導入する場合は、段階的な移行計画が必要です。

まとめ

  • Branded Types はTypeScriptの型安全性を向上させる強力な手法です。
  • TypeScriptの構造的型付けが持つ限界を克服し、名前的型付けの利点を享受できます。
  • 実装はunique symbolやリテラル型を使い、簡単に行うことができます。

日々の開発にBranded Typesを取り入れて、より堅牢でメンテナンスしやすいコードベースを築いていきましょう。

7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?