型安全、どうやって保証してる?
みなさんは普段どのように型の安全性を保証していますか?
TypeScriptを使っている方なら、型定義や型注釈、型推論で「型安全」を意識しているはずです。
しかし、実際のアプリケーション開発では「本当に型安全か?」と問われると、少し不安になることも多いのではないでしょうか。
Zodのスキーマ定義・型生成・データ検証
TypeScript界隈で人気の型安全ライブラリ「Zod」。
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の組み合わせを試してみてください!
Views: 0