月曜日, 6月 23, 2025
月曜日, 6月 23, 2025
- Advertisment -
ホームニューステックニュースZod + Branded Typeで真の型安全へ

Zod + Branded Typeで真の型安全へ



型安全、どうやって保証してる?

みなさんは普段どのように型の安全性を保証していますか?
TypeScriptを使っている方なら、型定義や型注釈、型推論で「型安全」を意識しているはずです。
しかし、実際のアプリケーション開発では「本当に型安全か?」と問われると、少し不安になることも多いのではないでしょうか。

Zodのスキーマ定義・型生成・データ検証

TypeScript界隈で人気の型安全ライブラリ「Zod」。
Zodを使えば、スキーマ定義・型生成・データ検証が一気通貫で行えます。

https://www.npmjs.com/package/zod

例えば「idは5桁かつ英数字のみ、nameは3文字以上15文字以下の文字列」としてUserSchemaを定義するとき、以下のようにスキーマ定義・型生成・データ検証ができます。

zod-basic-example.ts

import { z } from "zod";


const UserSchema = z.object({
  id: z.string().regex(/^[a-zA-Z0-9]{5}$/), 
  name: z.string().min(3).max(15), 
});


type User = z.infertypeof UserSchema>;


const userNG: User = UserSchema.parse({ id: "hoge!@#", name: "yu" }); 
const userOK: User = UserSchema.parse({ id: "abc45", name: "Taro" }); 

Zodだけでは型安全は不十分

Zodは強力ですが、実は「Zodだけ」では型安全が不十分なケースがあります。

たとえば、id: z.string()id: z.string().regex(/^[a-zA-Z0-9]{5}$/)でスキーマを定義しても、型レベルではどんな文字列でもidとして通ってしまいます。

zod-limitation-example.ts

import { z } from "zod";


const UserSchema = z.object({
  id: z.string().regex(/^[a-zA-Z0-9]{5}$/), 
  name: z.string().min(3).max(15), 
});


type User = z.infertypeof UserSchema>;


const user: User = { id: "!abcde12345", name: "a" }

idもnameもスキーマ定義に反した値ですが、型レベルで見るとどちらもstringなので、直接代入すると間違った値も代入できてしまいます。

inferred-type-example.ts


type User = {
  id: string;
  name: string;
} 

Branded Typeとは

この問題を解決するのが「Branded Type(以下、ブランド型)」です。
ブランド型を使うことで、同じstring型でも「意味の違い」を型レベルで区別できます。

Branded Typeの使い方

ブランド型の基本形は次の通りです。

branded-type-basic.ts

type UserId = string & { readonly _brand: "user_id" };
type OrderId = string & { readonly _brand: "order_id" };

function createUserId(): UserId {
  
  return id as UserId;
}

function createOrderId(): OrderId {
  
  return id as OrderId;
}

const userId: UserId = createUserId(); 
const orderId: OrderId = createOrderId(); 


const invalid: UserId = orderId; 

このように、UserId型とOrderId型はどちらもstringですが、ブランド型を付与することで型レベルで区別でき、意図しない値の混入を防げます。

Zodではv3.18.0以降でbrandメソッドを使ってスキーマにブランド型を付与できます。

zod-brand-example.ts

import { z } from "zod";


const UserSchema = z
  .object({
    id: z.string().regex(/^[a-zA-Z0-9]{5}$/), 
    name: z.string().min(3).max(15), 
  })
  .brand"User">(); 

Zod + Branded Typeで真の型安全へ

Zodのスキーマ定義・型生成・parseメソッドにブランド型を組み合わせることで、真の型安全を実現できます。

zod-branded-complete.ts

import { z } from "zod";


const UserSchema = z
  .object({
    id: z.string().regex(/^[a-zA-Z0-9]{5}$/), 
    name: z.string().min(3).max(15), 
  })
  .brand"User">(); 


type User = z.infertypeof UserSchema>;


const userNG: User = UserSchema.parse({ id: "hoge!@#", name: "yu" }); 
const userOK: User = UserSchema.parse({ id: "abc45", name: "Taro" }); 


const user: User = { id: "abc45", name: "Taro" } 

parseメソッドを使うことで、パース後の値には自動的に[BRAND]が付与されるようになります。
直接代入した場合、[BRAND] が存在しないためエラーとなります。

このようにZodのbrandを使うことで、正しい値をparseメソッドに通したものだけがデータ検証を通過し、型安全をさらに強化できます。

実際のユースケース

より実践的な例を見てみましょう。

APIレスポンス処理での活用例

api-response-example.ts

import { z } from "zod";


const ApiUserSchema = z
  .object({
    id: z.string().min(1),
    email: z.string().email(),
    name: z.string().min(1),
    role: z.enum(["admin", "user", "guest"]),
  })
  .brand"ApiUser">();

type ApiUser = z.infertypeof ApiUserSchema>;


async function fetchUser(id: string): PromiseApiUser> {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();
  
  
  return ApiUserSchema.parse(data);
}


async function handleUser() {
  try {
    const user = await fetchUser("123");
    
    console.log(user.name); 
  } catch (error) {
    console.error("Invalid user data:", error);
  }
}
フォームバリデーションでの活用例

form-validation-example.ts

import { z } from "zod";


const ContactFormSchema = z
  .object({
    name: z.string().min(1, "名前は必須です").max(50, "名前は50文字以内で入力してください"),
    email: z.string().email("有効なメールアドレスを入力してください"),
    message: z.string().min(10, "メッセージは10文字以上で入力してください"),
  })
  .brand"ContactForm">();

type ContactForm = z.infertypeof ContactFormSchema>;


function submitContactForm(formData: unknown): ContactForm {
  
  return ContactFormSchema.parse(formData);
}


function handleFormSubmit(rawData: unknown) {
  try {
    const validatedData = submitContactForm(rawData);
    
    sendEmail(validatedData);
  } catch (error) {
    
    console.error("Form validation failed:", error);
  }
}

メリット

Zod + Branded Typeの組み合わせによる主なメリット:

  • コンパイル時と実行時の両方で型安全性を保証
  • 意図しないデータの混入を防止
  • APIレスポンスやフォームデータの検証が確実
  • リファクタリング時の安全性向上
  • チーム開発での品質向上

Zodは型安全の第一歩ですが、ブランド型を組み合わせることで 「意味まで含めた真の型安全」 を実現できます。
型安全にこだわるなら、ぜひZod + Branded Typeの組み合わせを試してみてください!



Source link

Views: 0

RELATED ARTICLES

返事を書く

あなたのコメントを入力してください。
ここにあなたの名前を入力してください

- Advertisment -