投稿日:2026/8/2
更新日:2026/8/2

satisfies z.ZodType<T>)Zod と TypeScript の型を併用するとき、どちらを正にするかで向きが 2 つある。
| 向き | 正(原本) | 書き方 |
|---|---|---|
| スキーマ → 型 | Zod スキーマ | type User = z.infer<typeof userSchema> |
| 型 → スキーマ | TypeScript の型 | const userSchema = z.object({ ... }) satisfies z.ZodType<User> |
基本は前者。一度書けば型が生えるので二重に書く必要がなく、ズレも原理的に起きない。
この記事は後者の話。型を先に設計したい、あるいは型が外部から与えられて動かせない、というときに末尾の satisfies でスキーマを型に突き合わせる。この satisfies が何をしているのか、「Zod がチェックしているのに二重チェック?」という疑問に答える。
satisfies そのものの位置づけ(型注釈 / as / as const との比較)→ 型指定についてsatisfies とはTypeScript 4.9 で入った演算子。「この式は型 T を満たすか」を検査するだけで、型を T に変えない。
type User = { name: string; age: number };
const a: User = { name: 'x', age: 1 }; // ① 型注釈
const b = { name: 'x' } as User; // ② as
const c = { name: 'x', age: 1 } satisfies User; // ③ satisfies
| 間違いを検出できるか | 推論された具体的な型が残るか | |
|---|---|---|
: T(型注釈) |
✅ | ❌ T に潰れる |
as T |
❌ 嘘をつける | — |
satisfies T |
✅ | ✅ 残る |
②は age が無いのにコンパイルが通る。as が危険と言われる理由で、多くのプロジェクトで禁止されている。その代替が③。
const colors = { red: '#f00', blue: '#00f' } satisfies Record<string, string>;
colors.red; // ✅ キーが 'red' | 'blue' に絞られている
colors.green; // ✅ エラー(存在しないキー)
const colors2: Record<string, string> = { red: '#f00', blue: '#00f' };
colors2.green; // ⚠️ エラーにならない(Record<string, string> なので何でも許される)
satisfies は「型 T を満たすことは確かめたい、でも変数の型は具体的なままにしたい」ときの道具。
Zod スキーマに型注釈を付けると、ZodType に潰れてメソッドが使えなくなる。
// ❌ 型注釈:ZodType に潰れる
const s1: z.ZodType<{ a: string }> = z.object({ a: z.string() });
s1.extend({ b: z.number() });
// ^^^^^^ プロパティ 'extend' は型 'ZodType<...>' に存在しません
// ✅ satisfies:ZodObject のまま
const s2 = z.object({ a: z.string() }) satisfies z.ZodType<{ a: string }>;
s2.extend({ b: z.number() }); // 通る
s2.shape.a; // 通る
s2.partial(); // 通る
.extend() .shape .partial() .omit() などは ZodObject 固有のメソッドで、ZodType には無い。スキーマを組み合わせて使うなら型注釈は選べない。
「TypeScript の型」と「Zod スキーマ」を両方手で書く構成のとき、片方だけ直すとズレる。satisfies はそれをコンパイルエラーにする。
type User = {
name: string;
age: number; // ← 後から追加した
};
const userSchema = z.object({
name: z.string(),
// age を追加し忘れた
}) satisfies z.ZodType<User>;
// ❌ 型 'ZodObject<{ name: ZodString }>' は 'ZodType<User>' を満たしていません。
// プロパティ 'age' が型 '{ name: string }' にありません。
型に足したのにスキーマを直し忘れる、が即座に分かる。 これが最大の効能。
ただし逆向き(スキーマにだけ足した)は検出されない。satisfies は代入可能性の検査なので、多い分には通ってしまう。
type User = { name: string };
const userSchema = z.object({
name: z.string(),
age: z.number(), // ← 型に無いものを足した
}) satisfies z.ZodType<User>;
// ✅ 通ってしまう({ name: string; age: number } は { name: string } に代入できる)
何が検出でき、何がすり抜けるかは codegenの型からZodスキーマを作る に整理してある。
よくある疑問:Zod が検証しているのに、satisfies でもチェックするのは無駄では?
答えは「チェックする対象もタイミングも別物」。
const schema = z.object({ name: z.string() }) satisfies z.ZodType<User>;
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 実行時にデータを検証する実体
// ~~~~~~~~~~~~~~~~~~~~~~~~~ コンパイル時だけの検算
| Zod スキーマ | satisfies |
|
|---|---|---|
| いつ動く | 実行時 | コンパイル時のみ |
| 何を見る | 届いたデータ(JSON、フォーム入力) | ソースコード |
| 何を守る | 想定外のデータが入り込まないこと | 型定義とスキーマがズレないこと |
| ビルド後 | 残る | 消える |
satisfies は JavaScript に存在しない構文なので、コンパイルすると跡形もなく消える。
// ソース
const schema = z.object({ name: z.string() }) satisfies z.ZodType<User>;
// ビルド後の .js
const schema = z.object({ name: z.string() });
実行時に検証が2回走ることはない。 同じデータを2回見ているのではなく、「データ」と「コード」という別のものを見ている。
z.ZodType<T> は出力型しか見ない(v4)Zod v4 の型は ZodType<Output, Input> の2つのジェネリクスを持つ。
interface ZodType<out Output = unknown, out Input = unknown, ...>
z.ZodType<User> と書くと Input は unknown のままなので、入力側の型は検査されない。
v3 は挙動が違う。 v3 は
ZodType<Output, Def, Input>で第2引数がDef、InputのデフォルトがOutputなので、z.ZodType<User>でも入力側が検査される。下の.default()の例は v3 ではエラーになる。詳細は codegenの型からZodスキーマを作る。
.default() .optional() .transform() を使うと入力と出力がずれるため、ここが問題になる。
type Config = { retryCount: number };
const configSchema = z.object({
retryCount: z.number().default(3),
}) satisfies z.ZodType<Config>; // ✅ 通ってしまう
type Out = z.output<typeof configSchema>; // { retryCount: number } ← Config と一致
type In = z.input<typeof configSchema>; // { retryCount?: number } ← 省略できる
パース結果は Config と一致するので satisfies は通るが、受け付ける入力は Config ではない。「satisfies z.ZodType<Config> が付いているから、入力も Config の形のはず」と読むと誤読になる。
satisfies z.ZodType<Config, Config>
第2引数に入力型を書けば検査される。ただし .default() などを使っていると当然エラーになるので、「入出力が同じ形であること」を要求したいときに使う。
冒頭の 2 つの向きを並べる。
// スキーマ → 型:Zod が正、型は導出
export const userSchema = z.object({
name: z.string(),
age: z.number(),
});
export type User = z.infer<typeof userSchema>;
// 型 → スキーマ:型が正、スキーマを突き合わせる
export type User = { name: string; age: number };
export const userSchema = z.object({
name: z.string(),
age: z.number(),
}) satisfies z.ZodType<User>;
スキーマ → 型(z.infer) |
型 → スキーマ(satisfies) |
|
|---|---|---|
| 記述量 | 一度だけ | 二重に書く |
| ズレ | 原理的に起きない | satisfies が防ぐ(ただし §3 の片方向だけ) |
| 型の読みやすさ | 推論結果が展開されて読みにくいことがある | 手書きなので読みやすい |
| 型を先に設計できるか | スキーマから考えることになる | できる |
| 複雑な union | 推論任せで追いにくい | 意図が明示される |
| 型が外部から与えられる場合 | 使えない | これしかない |
記述量では z.infer 一択。 それでも satisfies 側を採る理由は 2 つある。
(a) 型定義を人間が読める形で残したいとき。 特に union で差が出る。
// 型 → スキーマ:何が排他なのか一目で分かる
export type Payment =
| { method: 'card'; cardNumber: string }
| { method: 'bank'; accountNumber: string };
// スキーマ → 型:IDE でホバーすると推論結果が展開され、読みにくくなりがち
export type Payment = z.infer<typeof paymentSchema>;
(b) そもそも型を動かせないとき。 codegen が吐いた型にフォームの値を渡す、といったケースでは向きを選ぶ余地がなく、スキーマ側を型に追従させるしかない。→ codegenの型からZodスキーマを作る
(a) は好みの問題なので、プロジェクトで統一されていることが大事。混在すると「この型はどっちが正なのか」が分からなくなる。(b) は選択の余地がないので、統一ルールから外れる例外として明示しておくとよい。
このリポジトリの schema/CLAUDE.md は「型 → スキーマ」の向きを採り、次のように定めている。
- Each file exports both the TypeScript types and the Zod schemas for the same resource
- Every Zod schema must end with
satisfies z.ZodType<T>
「必ず末尾に satisfies を付ける」と機械的に決めておくと、レビューで判断に迷わず、付け忘れも lint で検出できる。
z.infer)。この記事は逆向きの型 → スキーマ(satisfies)**の話satisfies は検査だけして型を変えない演算子。as の安全な代替.extend() などが使える)satisfies はコンパイル時のコードを見ている。ビルド後に satisfies は消えるz.ZodType<T> は出力型しか見ない。.default() などがあると入力型は違う