YASD-TECH
YASD TECH
# TypeScript

型からZodスキーマを作る

投稿日:2026/8/2

更新日:2026/8/2

ttitleImage

型から Zod スキーマを作る(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 がチェックしているのに二重チェック?」という疑問に答える。


1. satisfies とは

TypeScript 4.9 で入った演算子。「この式は型 T を満たすか」を検査するだけで、型を T に変えない。

ts
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 が危険と言われる理由で、多くのプロジェクトで禁止されている。その代替が③。

「型が残る」の意味

ts
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 に潰れてメソッドが使えなくなる。

ts
// ❌ 型注釈: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 はそれをコンパイルエラーにする。

ts
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 は代入可能性の検査なので、多い分には通ってしまう。

ts
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スキーマを作る に整理してある。


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

よくある疑問:Zod が検証しているのに、satisfies でもチェックするのは無駄では?

答えは「チェックする対象もタイミングも別物」。

ts
const schema = z.object({ name: z.string() }) satisfies z.ZodType<User>;
//             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 実行時にデータを検証する実体
//                                           ~~~~~~~~~~~~~~~~~~~~~~~~~ コンパイル時だけの検算
Zod スキーマ satisfies
いつ動く 実行時 コンパイル時のみ
何を見る 届いたデータ(JSON、フォーム入力) ソースコード
何を守る 想定外のデータが入り込まないこと 型定義とスキーマがズレないこと
ビルド後 残る 消える

satisfies は JavaScript に存在しない構文なので、コンパイルすると跡形もなく消える。

ts
// ソース
const schema = z.object({ name: z.string() }) satisfies z.ZodType<User>;
js
// ビルド後の .js
const schema = z.object({ name: z.string() });

実行時に検証が2回走ることはない。 同じデータを2回見ているのではなく、「データ」と「コード」という別のものを見ている。


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

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

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

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

v3 は挙動が違う。 v3 は ZodType<Output, Def, Input> で第2引数が DefInput のデフォルトが Output なので、z.ZodType<User> でも入力側が検査される。下の .default() の例は v3 ではエラーになる。詳細は codegenの型からZodスキーマを作る

.default() .optional() .transform() を使うと入力と出力がずれるため、ここが問題になる。

ts
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 の形のはず」と読むと誤読になる。

入力側も縛りたい場合

ts
satisfies z.ZodType<Config, Config>

第2引数に入力型を書けば検査される。ただし .default() などを使っていると当然エラーになるので、「入出力が同じ形であること」を要求したいときに使う。


6. どちらの向きを選ぶか

冒頭の 2 つの向きを並べる。

ts
// スキーマ → 型: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 で差が出る。

ts
// 型 → スキーマ:何が排他なのか一目で分かる
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) は選択の余地がないので、統一ルールから外れる例外として明示しておくとよい。


7. 実務上のルール例

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


まとめ

  1. Zod と型の関係には向きが 2 つある。基本は**スキーマ → 型(z.infer。この記事は逆向きの型 → スキーマ(satisfies)**の話
  2. satisfies検査だけして型を変えない演算子。as の安全な代替
  3. Zod で使う理由は2つ
    • スキーマの型を潰さない.extend() などが使える)
    • 型定義とスキーマのズレを検出する
  4. Zod との「二重チェック」ではない。Zod は実行時のデータ、satisfies はコンパイル時のコードを見ている。ビルド後に satisfies は消える
  5. z.ZodType<T>出力型しか見ない.default() などがあると入力型は違う
  6. 向きの選択は、型を読みやすく残したいか(好みの問題・要統一)と、型が外部から与えられて動かせないか(選択の余地なし)の 2 軸で決まる

Index

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