JSON 工具箱

Schema 怎麼產生 TypeScript 型別

結論:用 json-schema-to-typescript 或 quicktype 一行指令就能產出 .d.ts,但產生結果只是起點:可選欄位、聯合型別、`additionalProperties` 的對映都需要人工確認,否則型別看著對、用起來處處報錯。

進階約 4 分鐘發佈於 2026-10-30更新於 2026-10-30
從 JSON Schema 產生 TypeScript 型別的流程與坑

結論:用 json-schema-to-typescript 或 quicktype 產生只算第一步,隨後必須人工確認可選欄位、聯合型別、additionalProperties 與日期格式;Schema 是來源、型別是產物,並在 CI 加 types:check。

一行指令就能出結果,但產生的 .d.ts 只是草稿,不是成品。

最快的一行指令

npx json-schema-to-typescript schema.json > src/types/order.d.ts

它支援 $ref、allOf、oneOf、anyOf,會把 description 轉成 JSDoc 註解。需要一次產生多個檔案、加 banner、控制 style(interface 或 type)時,寫個 build-types.mjs 呼叫它的 API 更合適。

另一個常用工具是 quicktype:

npx quicktype -s schema schema.json -o order.ts --lang typescript

它也能直接吃範例 JSON 推型別,適合多語言專案統一產生。

產生之後必須確認的四件事

一、可選欄位的邊界。 Schema 裡沒寫進 required 的欄位,產生器一律標 ?。但「欄位必須存在、值可以是 null」和「欄位可以不存在」是兩回事:

interface Order {
  remark?: string | null;
  status: string;
}

如果後端實際總是回 remark: null,把 ? 去掉。

二、oneOf 產生的聯合型別。 產生器通常直接輸出 A | B,但兩個分支沒有可辨識欄位(discriminator)時,TypeScript 無法收斂,使用時還得寫型別守衛。建議在 Schema 裡加一個固定的 type 欄位。

三、additionalProperties。 寫成 true 或省略時,有的產生器會產出 [k: string]: unknown,有的是 any。前者安全,後者讓整個物件失去型別檢查。

四、格式欄位。 format: "date-time" 在 JSON 裡是字串,產生器預設給 string。要改成 Date 就得自己接管反序列化——不做這一步,執行時拿到的還是字串,型別寫著 Date 反而製造 bug。

方向要定死

Schema 與型別的關係必須是單向的:Schema 是來源,型別是產物。

反過來做(先寫 TS 型別,再用工具產生 Schema)會導致兩邊都能改,最終沒人知道哪邊是對的。確實需要從型別產生 Schema 時,也要約定「型別改動後必須立即重新產生並提交 Schema」,並在 CI 裡加一致性檢查。

放進流水線

"types:gen": "json2ts -i schemas/ -o src/types/generated",
"types:check": "pnpm types:gen && git diff --exit-code src/types/generated"

types:check 是關鍵:Schema 改了但沒重新產生,CI 直接失敗。沒有這一步,產生的型別遲早和 Schema 脫節。

常見問題

產生出來的型別為什麼會出現 any?

多來自 additionalProperties 寫成 true 或被省略,有的產生器會產出 [k: string]: any。看到 any 就應改成 unknown 或補全具體欄位。

應該如何確認可選欄位的邊界?

Schema 裡沒寫進 required 的欄位一律被標 ?,但「欄位必須存在、值可以是 null」和「欄位可以不存在」是兩回事。後端實際總是回傳該欄位時,把 ? 去掉。

是否可以讓 CI 檢查型別與 Schema 是否同步?

可以。加一條 types:check:重新產生後執行 git diff --exit-code,Schema 改了卻沒重新產生時 CI 直接失敗。

延伸閱讀