JSON 轉 TypeScript 實務
由 JSON 範本產生 TypeScript 型別,處理選擇性、可空值、聯合型別、巢狀型別,並結合執行期驗證策略。
當你呼叫一個回傳 JSON 的 API,TypeScript 需要對應的型別。一個有 40 欄、5 層巢狀的回應若靠手寫 interface,既痛苦又容易出錯。從範本生成 只要幾秒。本文涵蓋實務流程:從範本到型別、生成器常犯的錯,以及如何 加上執行期驗證,避免你的型別變成謊言。
為什麼要從 JSON 生型別
放棄手寫的兩個理由:
- 完整性——生成器看得到範本中的每個欄位,不會漏掉你掃讀時跳過的欄位。
- 同步性——API 加欄位時,重新生成只要 5 秒。手寫型別會腐爛。
代價是——生成器只根據單一範本推論型別。若範本沒包含某個 nullable 欄位 為 null 的情況,生成器會標成非 nullable。輸出永遠需要審視。
從範本到 interface
給定這份 JSON:
{
"id": "user_123",
"email": "ada@example.com",
"age": 36,
"active": true,
"tags": ["admin", "founder"],
"address": {
"street": "10 Downing",
"city": "London"
}
}
合理的輸出:
interface User {
id: string;
email: string;
age: number;
active: boolean;
tags: string[];
address: Address;
}
interface Address {
street: string;
city: string;
}
工具:
- JSON 轉 TypeScript 生成器——貼範本、拿型別。
- quicktype——同核心引擎,提供 CLI 與函式庫。
- VS Code: "Paste JSON as Code" 擴充套件。
處理選擇性與可空欄位
生成器看的是單一範本。兩件事幾乎都要人工修:
選擇性欄位。若某欄有時出現、有時不在,你要的是 field?: T 而非
field: T。提供多份範本,多數工具會合併並從「沒每份都出現」推論為
optional。
可空欄位。可能是 null 的欄位應標成 T | null:
interface User {
// 有時出現、有時不在
middleName?: string;
// 永遠出現,有時為 null
deletedAt: string | null;
// 可能不存在,也可能為 null
avatarUrl?: string | null;
}
三者意義不同,TypeScript 對差異很嚴格。不確定時請看 API 文件,而非 範本。
由異質陣列產出聯合型別
當陣列包含不同形狀的物件,生成器會發出聯合:
{
"events": [
{ "kind": "click", "target": "button-1" },
{ "kind": "page-view", "path": "/home" }
]
}
type Event =
| { kind: "click"; target: string }
| { kind: "page-view"; path: string };
字面型別 "click"(而非 string)讓它變成 判別聯合,TypeScript
能依 kind 收斂:
function handle(event: Event) {
if (event.kind === "click") {
event.target; // 推論為 string
} else {
event.path; // 推論為 string
}
}
若生成器吐出 kind: string 而非 kind: "click",請手動緊縮。判別子
幾乎都值得收斂。
命名與巢狀型別
生成器以父欄位命名巢狀型別——User.address 變 Address,
Order.shippingAddress 變 ShippingAddress。兩個問題:
- 衝突——文件不同地方各有一個
Address。請改名(BillingAddress、ShippingAddress)。 - 內聯 vs 命名——一次性的小型別內聯即可。跨多個 endpoint 共用的,給名字並 export。
/zh-Hant/json/types/typescript 可指定 根名稱並依字母排序輸出所有巢狀型別。
以 as const 鎖死字面型別
若 JSON 是 fixture(測試資料、設定),希望 TypeScript 知道 確切的
值 而不只是型別,用 as const:
const config = {
env: "production",
retries: 3,
features: ["billing", "auth"],
} as const;
// 型別: { readonly env: "production"; readonly retries: 3; readonly features: readonly ["billing", "auth"] }
對 switch 與 exhaustiveness check 很有用。
執行期驗證 vs 編譯期型別
生成的型別是 編譯期合約。執行期 API 可能回傳任何東西。直接 cast 就是說謊:
const user = JSON.parse(text) as User; // ← 謊言
…日後 user.email 為 undefined 時會出現詭異錯誤。
穩健的模式是用執行期驗證器一次定義形狀,再由它推出 TypeScript 型別。以 Zod:
import { z } from "zod";
const User = z.object({
id: z.string(),
email: z.string().email(),
age: z.number().int(),
active: z.boolean(),
tags: z.array(z.string()),
address: z.object({
street: z.string(),
city: z.string(),
}),
});
type User = z.infer<typeof User>;
function parseUser(text: string): User {
return User.parse(JSON.parse(text)); // 形狀不符即拋出
}
類似函式庫:
- Yup——較老,類 Joi API。
- io-ts——fp-ts 生態,較冗長、較強。
- Valibot——比 Zod 小 bundle,API 相似。
@sinclair/typebox——以 JSON Schema 為核心,能輸出合法 JSON Schema。
解析安全性深入內容請看 在 JavaScript 中安全解析 JSON。
與 API 保持同步
由便宜到可靠:
- 手動——貼範本、重新生成、commit。小型或變動少的 API 可。
- schema 驅動——若 API 已發佈 JSON Schema 或 OpenAPI,以
openapi-typescript或json-schema-to-typescript直接生型別。schema 是合約,型別由它衍生。 - 端到端——tRPC、GraphQL Code Generator,或前後端共享型別套件。編譯器替你把兩端鎖在一起。
策略應視「漂移成本」而定。對你不擁有的公開 REST API,手動再生就夠了; 團隊自有 API 建議走 schema 驅動或型別共享。
動手生
把範本貼進 JSON 轉 TypeScript 工具, 即可取得 TypeScript、Go、Python、Rust、Java、Swift、Kotlin、C# 的型別。 搭配執行期驗證器即為生產安全的模式。
延伸閱讀
- 在 JavaScript 中安全解析 JSON——執行期驗證模式。
- REST API 的 JSON 最佳實務——設計你要型別化的 payload。