🍣
valibot や zod でオブジェクトに対して brand (Branded Types) を使う時の注意点
問題になる使い方
以下のように 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