Tech

Tips

TypeScript

【TypeScript】Zod で実現!型安全性を強化するブランド型(Branded Type)の活用法

2025年10月4日

2025年10月4日

【TypeScript】Zod で実現!型安全性を強化するブランド型(Branded Type)の活用法

こんにちは、けいこんぐらです!

JavaScript は柔軟で便利ですが、その反面、型の不一致や予期せぬ値の扱いでバグが発生しやすいという弱点もあります。(実行するまでわからない、、🤦)

そこで登場したのが型という概念を組み込んだ TypeScript です!!

JavaScript の弱みであった型による静的チェックを導入することで、バグの早期発見が可能になりました 🤩

ただし、TypeScript には同じ構造の型は同じものとして扱うという特徴があります。

これが思わぬバグにつながることも、あるとかないとか、、🧐

JavaPHP に慣れ親しんでいる方からすると、違和感のある型構造かもしれません。

例えば、「ユーザー」と「商品」を区別したい場合、同じ構造である限りはエラーにはなりません。

TypeScriptの例
class User {
  constructor(public id: number, public name: string) {}
}
 
class Product {
  constructor(public id: number, public name: string) {}
}
 
// User と Product はどちらも id, name を持つクラスなので、「互換性」があるとみなされる
// エラーにはならない!
const user: User = new Product(1, "Product");
 
console.log(user);
// Output: Product { id: 1, name: 'Product' }

User 型の変数に「商品」のデータを入れても動作します 💦

そこで役立つのがブランド型(Branded Type)です。幽霊型という呼び方もあるみたいですね!

今回は、TypeScript の型システムの特徴を理解しつつ、ブランド型を使って型の区別を強化する方法を解説していきます!

2 パターンの型区別

型の区別には大きく分けて以下の 2 パターンあります。

  • 名前的型付け(Nominal Typing)
  • 構造的型付け(Structural Typing)

名前的型付け(Nominal Typing)

型の名前が一致するかどうかで互換性を判断する方式で、構造が同じでも名前が違えば別物として扱うのが特徴です 🙆‍♂️

📝 採用している言語

  • Java
  • PHP
  • C# など
Javaの例
class User {
    public int id;
    public String name;
 
    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }
}
 
class Product {
    public int id;
    public String name;
 
    public Product(int id, String name) {
        this.id = id;
        this.name = name;
    }
}
 
public class Main {
    public static void main(String[] args) {
        // 型の"名前"が違うのでエラーになる
        User user = new Product(1, "Product");
        // ⚠️ error: incompatible types: Product cannot be converted to User
    }
}

構造的型付け(Structural Typing)

型の構造が同じなら互換性ありとみなす方式で、名前が違っても構造が同じなら代入可能となります。

📝 採用している言語

  • Go
  • TypeScript など

サンプルは 上記の「TypeScript の例」を参照ください!

この柔軟さのおかげで直感的なオブジェクトの操作や、一時的な型の定義が容易になります ✨

特に、モックの作成時とかに、適当なオブジェクトを作りたいときとかはかなり楽ですね!

個人的に微妙なところ

これは個人的にですが、構造的型付けの思想自体は肯定派ですが、TypeScript の構造的部分型という機能が厄介だなと感じるシーンが多々あります、、

例えば、こんなやつ!(結構これを見かけるシーンは多いんじゃないでしょうか!)

TypeScriptの構造的部分型の例
type User = {
  id: number;
  name: string;
  age: number;
};
 
type Product = {
  id: number;
  name: string;
};
 
const user: User = {
  id: 1,
  name: "Alice",
  age: 30,
};
 
const product: Product = {
  id: 1,
  name: "Product",
};
 
function printProductName(product: Product) {
  console.log(product);
}
 
printProductName(user);
// Output: { id: 1, name: 'Alice', age: 30 }
// Product型に存在しない、ageまでもが渡ってしまう、、

このように、User 型は Product 型のスーパーセットであるため、User 型の値を Product 型のパラメータに渡すことができます。

つまり、構造が完全一致じゃなくても渡せる場合があるため、意図せず不要な値が渡る可能性があるんですね、、

これは危険だ 😵‍💫

なぜ構造的型付けなのか?

TypeScript が構造的型付けを採用する理由ですが、

それは、JavaScript が動的型付け言語であり、ダック・タイピングという型付けスタイルを取っているのが理由として挙げられるみたいです!

If it walks like a duck and quacks like a duck, it must be a duck(もしもそれがアヒルのように歩き、アヒルのように鳴くのなら、それはアヒルに違いない)

つまり、「そのオブジェクトが「ID」と「名前」を持っているなら、User ともみなせるし、Product ともみなせる」みたいなイメージでしょうか!

ブランド型(Branded Type)

ブランド型とは、型を区別するための意味を型に持たせることで、その型を別物として扱うテクニックです!

TypeScript の型システムだけで表現できる手法で、実行時には通常の値と変わりません!

これは良さそうですね 🥳

type UserId = {
  // UserIdというブランドを付与
  __userId: never;
  id: number;
};
 
type ProductId = {
  // ProductIdというブランドを付与
  __productId: never;
  id: number;
};
 
const printProductId = (userId: ProductId) => {
  console.log(userId);
};
 
// 型アサーションを使用して、UserIdとして定義する!
const userId = { id: 1 } as UserId;
 
// エラーが発生する
printProductId(userId);
// ⚠️ 型 'UserId' の引数を型 'ProductId' のパラメーターに割り当てることはできません。
// プロパティ '__productId' は型 'UserId' にありませんが、型 'ProductId' では必須です。

これで「ユーザー ID」と「商品 ID」を別物として区別できるようになります!

付与するブランドには意味のある名前をつけて、値を持たせる必要は有りません!

ただこれでもいいんですが、これだとasを使用して、無理やり構造を操作している感じがしますね 🫠

もっとスマートに!

上記を踏まえて、Zod を使用すれば、もっとスマートにブランド型を実装できます!

Intro | Zod

Introduction to Zod - TypeScript-first schema validation library with static type inference

https://zod.dev

Intro | Zod

ちなみに、Zod とは TypeScript 向けのスキーマ宣言およびバリデーションライブラリです!

npm install zod

Zod での実装例

Zod のbrand()メソッドを使用すれば、ブランド型を簡単に実装できます 👀

import { z } from "zod";
 
// 1. Zodのスキーマ定義 + brand()メソッドでブランド型を定義
const UserIdSchema = z.number().brand<"UserId">();
const ProductIdSchema = z.number().brand<"ProductId">();
 
// 2. スキーマから型を生成
type UserId = z.infer<typeof UserIdSchema>;
type ProductId = z.infer<typeof ProductIdSchema>;
// => type UserId = number & z.core.$brand<"UserId">
// => type ProductId = number & z.core.$brand<"ProductId">
 
// 3. スキーマの構造に一致するデータをパースすることで自動的にブランドを付与できる
const userId = UserIdSchema.parse(1);
const productId = ProductIdSchema.parse(2);
// => const userId: number & z.core.$brand<"UserId">
// => const productId: number & z.core.$brand<"ProductId">
 
function getUserById(userId: UserId) {
  console.log(userId);
}
 
function getProductById(productId: ProductId) {
  console.log(productId);
}
 
// OKパターン
getUserById(userId);
getProductById(productId);
 
// NGパターン
getUserById(productId);
// ⚠️ 型 'number & $brand<"ProductId">' の引数を型 'number & $brand<"UserId">' のパラメーターに割り当てることはできません。
// 型 'number & $brand<"ProductId">' を型 '$brand<"UserId">' に割り当てることはできません。
// プロパティ '[$brand]' の型に互換性がありません。
// プロパティ 'UserId' は型 '{ ProductId: true; }' にありませんが、型 '{ UserId: true; }' では必須です。
 
// 💭 ブランド型を判断して、しっかりとエラーを出してくれます!

更に、Zod を使用するメリットは、parse()メソッドを使用することで、型の検証も同時に行える点です!

なるべく、asを使いたくない私に取っては、Zod のようなランタイム検証と型安全性を両立できるライブラリは非常にありがたいです!

ブランド型が使える場面

個人的にはドメインモデルの型定義などで有効に活用できると感じています!

本ブログを例とする場合、記事データをドメインモデルとしてデザインして、API 経由で記事を取得してるのですが、こんな風にブランド型を活用しています!

Zodでブランド型を活用した例
import { z } from "zod";
 
// スキーマ定義 + brand()メソッドでブランド型を定義
const articleSchema = z
  .object({
    id: z.string().min(1),
    title: z.string().min(1),
    description: z.string().min(1),
    content: z.string().min(1),
    createdAt: z.date(),
    updatedAt: z.date(),
  })
  .brand("Article");
 
type Article = z.infer<typeof articleSchema>;
 
/**
 * 記事データを全件取得する
*/
export const findAll = async (): Promise<Article[]> => {
  const articles = await /* データフェッチ... */
 
  // 取得したデータをArticle型にパースして返却する
  return articles.map((article) =>
    articleSchema.parse({
      id: article.id,
      title: article.title,
      description: article.description,
      content: article.content,
      createdAt: new Date(article.createdAt),
      updatedAt: new Date(article.updatedAt),
    })
  );
};

実際の実装からは少し簡略化していますが、上記のようにすることで、findAll()メソッドで取得できるデータは、必ず型検証済みで、且つ必要なデータだけ保持している Article 型であることが担保できます 🚀

終わりに

構造的型付けは TypeScript の型システムの根幹で、ときに便利ですが、ときに意図しない型の互換性が発生することがあります。

ブランド型を適切に使用することで、構造的型付けの利便性を損なわずに、型の区別を強化できます!

Zod のbrand()を使えば実行時検証と型安全を両立でき、バグ予防や可読性向上に寄与します。

どのような場面で使用するかの見極めはある程度必要ですが、導入は軽量で効果も高いので、もしブランド型を初めて知ったよという方は、是非試してみてください!

前の記事

【GitHub】Issue ドリブン開発を個人開発に取り入れてみた話

Issueを中心に回す開発手法を個人プロジェクトで実践した体験談。ブランチ命名、PR連携、自動クローズやラベル運用など、すぐ使える運用例付き。

プロフィール

Profile Icon

けいこんぐら

ITエンジニア

大阪在住のサラリーマン。👨‍💻

新卒未経験でエンジニアとして働き、システムの開発等を行っている。

いつか、つよつよエンジニアになれるように日々頑張っています!

タグ