🍣

valibot や zod でオブジェクトに対して brand (Branded Types) を使う時の注意点

2024/10/29に公開

問題になる使い方

以下のように object に対して brand を適用する書き方をすると問題が発生することがあります

valibot
const User = v.pipe(
  v.object({
    name: v.string(),
    age: v.pipe(v.number(), v.minValue(0)),
  }),
  v.brand("User")
);
zod
const User = z
  .object({
    name: z.string(),
    age: z.number().min(0),
  })
  .brand<"User">();

このような書き方をすると、スプレッド構文を使用して不正な値を作成することができてしまいます
以下に例を示します

valibot
import * as v from "valibot";

const User = v.pipe(
  v.object({
    name: v.string(),
    age: v.pipe(v.number(), v.minValue(0)),
  }),
  v.brand("User"),
);

type User = v.InferOutput<typeof User>;

const user1 = v.parse(User, { name: "Alice", age: 20 });

// age を書き換えてもTypeScriptのエラーが出ない
// -> パース済みである保証ができない
const user2: User = { ...user1, age: -1 };
zod
import { z } from "zod";

const User = z
  .object({
    name: z.string(),
    age: z.number().min(0),
  })
  .brand<"User">();

type User = z.infer<typeof User>;

const user1 = User.parse({ name: "Alice", age: 20 });

// age を書き換えてもTypeScriptのエラーが出ない
// -> パース済みである保証ができない
const user2: User = { ...user1, age: -1 };

user2 の型は brand が付与された User 型なのでパース済みであることが期待されますが、実際にはパースされておらず不正な値が入ってしまっています

解決策

これを解決するには、object に対してではなく、個々のプロパティごとに brand を付与します

valibot
import * as v from "valibot";

const User = v.object({
  name: v.string(),
  age: v.pipe(v.number(), v.minValue(0), v.brand("Age")),
});

type User = v.InferOutput<typeof User>;

const user1 = v.parse(User, { name: "Alice", age: 20 });

// Error
// Type 'number' is not assignable to type 'number & Brand<"Age">'.
// Type 'number' is not assignable to type 'Brand<"Age">'.ts(2322)
const user2: User = {...user1, age: -1};
zod
import { z } from "zod";

const User = z.object({
  name: z.string(),
  age: z.number().min(0).brand<"Age">(),
});

type User = z.infer<typeof User>;

const user1 = User.parse({ name: "Alice", age: 20 });

// Error
// Type 'number' is not assignable to type 'number & BRAND<"Age">'.
// Type 'number' is not assignable to type 'BRAND<"Age">'.ts(2322)
const user2: User = { ...user1, age: -1 };

こうすることで User 型の値であれば、brand を付与したプロパティがパースされていることを保証できるようになります

めでたしめでたし

あとがき

僕は valibot が好きです

Discussion