投稿日:2026/7/26
更新日:2026/7/26

satisfies の併用export const userSchema = z.object({ ... }) satisfies z.ZodType<User>;
この末尾の 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 }' にありません。
型に足したのにスキーマを直し忘れる、が即座に分かる。 これが最大の効能。
逆向き(スキーマにだけ足した)も検出される。
type User = { name: string };
const userSchema = z.object({
name: z.string(),
age: z.number(), // ← 型に無いものを足した
}) satisfies z.ZodType<User>;
// ❌ 型が一致しない
よくある疑問: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> は出力型しか見ないZod の型は ZodType<Output, Input> の2つのジェネリクスを持つ。
interface ZodType<out Output = unknown, out Input = unknown, ...>
z.ZodType<User> と書くと Input は unknown のままなので、入力側の型は検査されない。
.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() などを使っていると当然エラーになるので、「入出力が同じ形であること」を要求したいときに使う。
z.infer で型を導出するそもそも二重に書かず、Zod スキーマから型を導出する方式もある。
// 方式B:Zod が主、型が従
export const userSchema = z.object({
name: z.string(),
age: z.number(),
});
export type User = z.infer<typeof userSchema>;
方式A(型が主 + satisfies) |
方式B(z.infer) |
|
|---|---|---|
| 記述量 | 二重に書く | 一度だけ |
| ズレ | satisfies が防ぐ |
原理的に起きない |
| 型の読みやすさ | 手書きなので読みやすい | 推論結果が展開されて読みにくいことがある |
| 型を先に設計できるか | できる | スキーマから考えることになる |
| 複雑な union | 意図が明示される | 推論任せで追いにくい |
方式Bの方が記述量は少ない。 それでも方式Aを採るのは、型定義を人間が読める形で残したいときが多い。
特に union では差が出る。
// 方式A:何が排他なのか一目で分かる
export type Payment =
| { method: 'card'; cardNumber: string }
| { method: 'bank'; accountNumber: string };
// 方式B:IDE でホバーすると推論結果が展開され、読みにくくなりがち
export type Payment = z.infer<typeof paymentSchema>;
どちらが正しいという話ではなく、プロジェクトで統一されていることが大事。混在すると「この型はどっちが正なのか」が分からなくなる。
このリポジトリの schema/CLAUDE.md は方式Aを採り、次のように定めている。
- 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 で検出できる。
satisfies は検査だけして型を変えない演算子。as の安全な代替.extend() などが使える)satisfies はコンパイル時のコードを見ている。ビルド後に satisfies は消えるz.ZodType<T> は出力型しか見ない。.default() などがあると入力型は違うz.infer で導出する方式もある。読みやすさを取るか記述量を取るかの選択で、プロジェクトで統一することが重要