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

結論:用 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 直接失敗。