YASD-TECH
YASD TECH
# TypeScript

Zodとsatisfiesの併用

投稿日:2026/7/26

更新日:2026/7/26

ttitleImage

Zod と satisfies の併用

export const userSchema = z.object({ ... }) satisfies z.ZodType<User>;

この末尾の satisfies が何をしているのか。「Zod がチェックしているのに、二重チェック?」という疑問に答える。

satisfies そのものの位置づけ(型注釈 / as / as const との比較)は 型指定について にまとめてある。


1. 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 を満たすことは確かめたい、でも変数の型は具体的なままにしたい」ときの道具。


2. Zod で使う理由 (1):スキーマの機能を殺さない

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 には無い。スキーマを組み合わせて使うなら型注釈は選べない。


3. Zod で使う理由 (2):型定義とのズレを検出する

「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>;
// ❌ 型が一致しない

4. 「二重チェック」ではない

よくある疑問: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回見ているのではなく、「データ」と「コード」という別のものを見ている。


5. 落とし穴:z.ZodType<T> は出力型しか見ない

Zod の型は ZodType<Output, Input> の2つのジェネリクスを持つ。

interface ZodType<out Output = unknown, out Input = unknown, ...>

z.ZodType<User> と書くと Inputunknown のままなので、入力側の型は検査されない

.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() などを使っていると当然エラーになるので、「入出力が同じ形であること」を要求したいときに使う。


6. もう一つの選択肢: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>;

どちらが正しいという話ではなく、プロジェクトで統一されていることが大事。混在すると「この型はどっちが正なのか」が分からなくなる。


7. 実務上のルール例

このリポジトリの 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 で検出できる。


まとめ

  1. satisfies検査だけして型を変えない演算子。as の安全な代替
  2. Zod で使う理由は2つ
    • スキーマの型を潰さない.extend() などが使える)
    • 型定義とスキーマのズレを検出する
  3. Zod との「二重チェック」ではない。Zod は実行時のデータ、satisfies はコンパイル時のコードを見ている。ビルド後に satisfies は消える
  4. z.ZodType<T>出力型しか見ない.default() などがあると入力型は違う
  5. z.infer で導出する方式もある。読みやすさを取るか記述量を取るかの選択で、プロジェクトで統一することが重要

Index

  • Zod と satisfies の併用
  • 1. satisfies とは
  • 「型が残る」の意味
  • 2. Zod で使う理由 (1):スキーマの機能を殺さない
  • 3. Zod で使う理由 (2):型定義とのズレを検出する
  • 4. 「二重チェック」ではない
  • 5. 落とし穴:z.ZodType<T> は出力型しか見ない
  • 入力側も縛りたい場合
  • 6. もう一つの選択肢:z.infer で型を導出する
  • 7. 実務上のルール例
  • まとめ