背景
TypeScript開発における不可欠な要素「型」、それを司るライブラリZodですが、2021年以来Zod3というメインバージョンをコミュニティで愛用されてきました。
昨晩(日本時間2025/04/11 0時頃)、Zod4が正式公表されましたので、早速使ってみて、面白いところを抜粋し、Zod3との違いも比較してみました。
準備
npm install zod@next
で、Zod4(beta)をインストールできます。
{
"devDependencies": {
"zod": "^4.0.0-beta.20250411T005215"
}
}
現時点(4/11日午後)は以上のバージョンになります。
では、始めましょう!
metaデータの型化/JSONSchema
const schema = z
.object({
name: z.string(),
id: z.number(),
})
.describe("This is a schema for a user object");
console.log(schema.description); // This is a schema for a user object
const schema = z
.object({
name: z.string(),
id: z.number(),
})
.meta({
description: "This is a schema for a user object",
examples: [
{ name: "John Doe", id: 123 },
{ name: "Jane Doe", id: 456 },
],
});
// {
// description: 'This is a schema for a user object',
// examples: [ { name: 'John Doe', id: 123 }, { name: 'Jane Doe', id: 456 } ]
// }
console.log(schema.meta());
もともとdescription
フィールドしかサポートしてないのに対して、Zod4のmetaデータは型化出来て表現の幅は大きく変わりました。
それに加えて、JOSNSchemaへの変換もZod4で行えるようになりました。
const jsonSchema = z.toJSONSchema(schema);
// {
// description: 'This is a schema for a user object',
// examples: [ { name: 'John Doe', id: 123 }, { name: 'Jane Doe', id: 456 } ],
// type: 'object',
// properties: { name: { type: 'string' }, id: { type: 'number' } },
// required: [ 'name', 'id' ]
// }
console.log(jsonSchema);
metaデータもきれいに反映されており、開発プロセスの手助けに間違いありませんね!
z.interface()
一言で言うと、TypeScirptの内部型と表現力の完全互換を目指す機能です。
type User = {
name: string;
id: number;
extra: string | undefined;
};
const userSchema = z.object({
name: z.string(),
id: z.number(),
extra: z.string().optional(),
});
// {
// name: string;
// id: number;
// extra?: string | undefined;
// }
type ZUser = z.infer<typeof userSchema>;
ここのUser
とZUser
、一見対等な型ではありますが、以下のシーンでは違いが生じます。
// type error!
// Property 'extra' is missing in type '{ name: string; id: number; }
const user1: User = {
name: "John Doe",
id: 123,
};
// valid!
const user2: ZUser = {
name: "John Doe",
id: 123,
};
オプションプロパティextra
に対して、extra?: string
とextra: string|undefined
二つの定義法がありますが、Zod3では両者の性質を持ってしまい、振る舞い上では前者に近い形です。
const userSchema = z.interface({
name: z.string(),
id: z.number(),
extra: z.string().optional(), // "extra?": z.string()
});
// type ZUser = {
// name: string;
// id: number;
// extra: string | undefined;
// }
type ZUser = z.infer<typeof userSchema>;
Zod4では、.optional()をプロパティに設定すれば、extra: string|undefined
と同じ振る舞いになります。z.interface()でextra?: string
みたいなプロパティを作りたい場合、extra?
をプロパティ名を提供する必要があります。
直感に沿った振る舞いですが、.optional()においてz.object()と違う振る舞いになりますので要注意です。
これとは別に、z.interface()の力で、再帰型の定義もやりやすくなりました。
見た目の変更
z.config(z.core.locales.en());
上記の一行で、エラーメッセージの言語変更もできるようになりました、と言っても現在英語しかサポートしておらず、有志の方はぜひコントリビューションしてください!
見た目に関して、もう一つの重要変更がありますーーZodErrorのカスタマイズ化です。
const myError = new z.ZodError([
{
code: "unrecognized_keys",
keys: ["extra"],
path: [],
message: "unrecognized keys!",
input: dataErr,
},
]);
// ✖ unrecognized key extra!
console.log(z.prettifyError(myError));
上記の記述で、簡潔なエラーメッセージが出力されます。またはやい段階ですが、長くネスト深くZodErrorで頭を悩まされる日も終わりが見えてきました。
@zod/mini・@zod/core
@zod/miniは、記述法が大きく変化する代わりに、無駄を極限まで減らし、bundle sizeを驚異の1.88kb
まで圧縮したminiバージョンです。
import * as z from "@zod/mini";
z.optional(z.string()); // z.string().optional();
z.union([z.string(), z.number()]); // z.string().or(z.number());
Zodとは記述法が真逆になったり、機能がなくなったりはしますが、bundle sizeに厳しいシーンでは有益なはずです。
@zod/coreはパッケージ開発者のために用意されたライブラリです。パッケージ間でZodタイプの受け渡しが簡単になったり、APIにZodタイプを渡したりなど、Zod本体の機能とはまた違いますが用途が多そうです。
最後に
「型」の最終ゴールは、「型」の存在をできるだけ気づかせないことだと、個人的に思っています。孔子が曰く、「心の欲するところに従えども矩を踰えず」と共通するんじゃないかなあと。
そのため、TypeScriptの内部型をゴリゴリカスタマイズし、常に型を意識させるより、Zodみたいなライブラリを用いて、一貫するスキーマに基づいたアプリを構築するのが極めて面白い方向性だと考えています。それだけで型の定義や型に向ける意識は無くならないけど、そのオーバーヘッドを最小限に抑えることが大きな意義をもたらします。
これからもZodの動向に注目していきます。