每個 TypeScript 開發者都遇過這種情況:你正在串接一個新的 API,手上有一份範例回應,在寫任何業務邏輯之前都需要先定義好型別化的 interface。手動撰寫這些 interface 既繁瑣又重複,還容易出錯 —— 尤其是回應巢狀很深或欄位數量眾多的時候。本文涵蓋將 JSON 轉換為 TypeScript interface 的各種實用方法:輸出應該長什麼樣、如何自動產生,以及如何處理棘手的情況。
為什麼 JSON 資料需要 TypeScript interface
在 TypeScript 中,不加型別註解就呼叫 API,解析得到的回應型別會是 any。這代表沒有自動完成、沒有型別檢查,也沒有編譯期的安全性。屬性名稱拼錯會變成執行時 bug,而不是編譯錯誤。
// 沒有型別 —— 一切都是 any
const response = await fetch('/api/users/42');
const user = await response.json(); // user: any
console.log(user.naem); // 拼字錯 —— 編譯時無錯誤
// 使用型別化的 interface
interface User {
id: number;
name: string;
email: string;
}
const response = await fetch('/api/users/42');
const user: User = await response.json();
console.log(user.naem); // 錯誤: Property 'naem' does not exist on type 'User'interface 在執行時沒有任何成本 —— 它只存在於編譯期。但它 完全改變了開發體驗。
JSON 轉 TypeScript 後會產生什麼
每個 JSON 物件都會變成一個 TypeScript interface,每種 JSON 原生型別會對應到對應的 TypeScript 型別,陣列則會推斷其元素型別。
// 輸入的 JSON
{
"id": 42,
"name": "Alice Johnson",
"email": "alice@example.com",
"active": true,
"score": 98.5,
"address": {
"street": "123 Main St",
"city": "New York",
"zip": "10001"
},
"tags": ["admin", "editor"],
"orders": [
{ "id": 101, "total": 49.99, "status": "shipped" },
{ "id": 102, "total": 99.00, "status": "pending" }
]
}// 輸出的 TypeScript
interface Root {
id: number;
name: string;
email: string;
active: boolean;
score: number;
address: Address;
tags: string[];
orders: Order[];
}
interface Address {
street: string;
city: string;
zip: string;
}
interface Order {
id: number;
total: number;
status: string;
}每個巢狀物件都會成為一個具名 interface。名稱來自父鍵 —— address 變成 Address,orders 中的元素變成 Order。這讓型別保持可讀且易組合。
型別對應規則
理解 JSON 型別如何對應到 TypeScript 型別,可讓輸出更可預測,也方便你放心地調整。
| JSON 值 | TypeScript 型別 |
|---|---|
"hello" | string |
42, 3.14 | number |
true, false | boolean |
null | null |
{} | 具名 interface |
['a', 'b'] | string[] |
[1, 'two'] | (number | string)[] |
[](空) | unknown[] |
一個小細節:TypeScript 沒有整數型別。42 和 3.14 都會變成 number。如果這個區別對你的用途很重要,可以使用品牌型別或加上註解。
方法一:使用線上 JSON 轉 TypeScript 工具
對於一次性任務 —— 新的 API 端點、快速原型 —— 線上工具是最快的方式。fixjson 的 JSON 轉 TypeScript 工具 完全在瀏覽器內完成轉換:
- 貼上你的 JSON 到輸入面板。工具接受合法 JSON,也接受帶有常見錯誤(如尾隨逗號、單引號)的損壞 JSON —— 它會在轉換前先修復。
- TypeScript interface 立即出現 在右邊 —— 無需按按鈕。
- 複製輸出,直接貼到你的 TypeScript 檔案中。
所有操作都在本地執行。任何資料都不會傳送到伺服器 —— 當你處理的 JSON 包含 API 金鑰、使用者資料或內部設定時,這點尤其重要。
方法二:用 TypeScript 自己寫轉換器
當你需要以程式方式產生型別 —— 作為建置腳本或程式碼產生器的一部分 —— 可以自己實作邏輯。以下是一個最小但完整的實作:
function jsonToTypeScript(value: unknown, rootName = 'Root'): string {
const interfaces: string[] = [];
const seen = new Set<string>();
function cap(s: string) {
return s ? s.charAt(0).toUpperCase() + s.slice(1) : 'Unknown';
}
function singular(name: string) {
if (/ies$/i.test(name)) return name.slice(0, -3) + 'y';
if (/[^s]s$/i.test(name)) return name.slice(0, -1);
return name + 'Item';
}
function getType(val: unknown, name: string): string {
if (val === null) return 'null';
if (typeof val === 'boolean') return 'boolean';
if (typeof val === 'number') return 'number';
if (typeof val === 'string') return 'string';
if (Array.isArray(val)) {
if (val.length === 0) return 'unknown[]';
const objs = val.filter(
(v): v is Record<string, unknown> =>
v !== null && typeof v === 'object' && !Array.isArray(v),
);
if (objs.length > 0) {
const itemName = cap(singular(name));
buildInterface(itemName, objs);
return `${itemName}[]`;
}
const types = [...new Set(val.map((v) => getType(v, name)))];
return types.length === 1 ? `${types[0]}[]` : `(${types.join(' | ')})[]`;
}
if (typeof val === 'object') {
const iName = cap(name);
buildInterface(iName, [val as Record<string, unknown>]);
return iName;
}
return 'unknown';
}
function buildInterface(name: string, objs: Record<string, unknown>[]): void {
if (seen.has(name)) return;
seen.add(name);
const allKeys = [...new Set(objs.flatMap((o) => Object.keys(o)))];
const props: string[] = [];
for (const key of allKeys) {
const present = objs.filter((o) => key in o);
const optional = present.length < objs.length ? '?' : '';
const propType = getType(present[0][key], cap(key));
const safeKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : `"${key}"`;
props.push(` ${safeKey}${optional}: ${propType};`);
}
interfaces.push(`interface ${name} {\n${props.join('\n')}\n}`);
}
const rootType = getType(value, rootName);
const defs = interfaces.reverse().join('\n\n');
if (rootType !== cap(rootName)) {
return `type ${cap(rootName)} = ${rootType};\n\n${defs}`.trim();
}
return defs;
}在 Node.js 建置腳本中使用:
import * as fs from 'fs';
const json = JSON.parse(fs.readFileSync('api-response.json', 'utf8'));
const typescript = jsonToTypeScript(json, 'UserResponse');
fs.writeFileSync('types/user.ts', typescript);方法三:quicktype(CLI 與函式庫)
quicktype 是功能最完整的開源工具,可從 JSON 產生 TypeScript 型別。它能處理簡單產生器會漏掉的邊界情況,並支援多種輸出格式。
npm install -g quicktype
# 從本地檔案
quicktype api-response.json -o types/user.ts --lang typescript
# 從 URL
quicktype https://api.example.com/users/42 -o types/user.ts --lang typescript
# 從 stdin
cat api-response.json | quicktype --lang typescriptquicktype 也能用 io-ts 或 zod 等函式庫產生執行時驗證函式 —— 當你需要驗證傳入資料而不僅是靜態型別時很有用。
處理棘手的情況
可為 null 的欄位
JSON API 常常用 null 表示缺失的值。產生器看到 null 時會把型別產生為 null —— 但你通常想要的是 string | null:
// 自動產生(第一版 —— 太窄)
interface Root {
name: string;
middleName: null;
}
// 你應該改成
interface Root {
name: string;
middleName: string | null;
}產生後務必手動檢查可為 null 的欄位。如果你有多份範例回應,某個欄位有時是字串、有時是 null,好的產生器會自動推斷出 string | null。
選擇性 vs 可為 null
TypeScript 區分 field?: string(鍵可能不存在)和 field: string | null(鍵一定存在,值可能為 null):
interface Order {
id: number;
total: number;
discount?: number; // 部分回應中沒有這個鍵
note: string | null; // 鍵一定存在,值可能為 null
}能分析多份 JSON 範例的產生器可以精準推斷出這個差別。單樣本產生器會把缺少的鍵標為 optional,但無法區分鍵不存在和值為 null。
不是合法識別字的鍵
有些 JSON 鍵包含 TypeScript 屬性名稱不合法的字元:
// JSON
{ "content-type": "application/json", "x-request-id": "abc123" }
// 產生的 TypeScript —— 合法,但需要用中括號取值
interface Root {
"content-type": string;
"x-request-id": string;
}
// 用中括號取值
obj["content-type"]如果你能掌控 API,優先使用駝峰式命名以徹底避免此問題。如果不行,加引號的屬性寫法在 TypeScript 中完全合法且可運作。
混合型別的陣列
// JSON
{ "values": [1, "two", true, null] }
// 產生結果
interface Root {
values: (number | string | boolean | null)[];
}這種聯合型別技術上正確,但通常代表 API 設計有問題。看到這種狀況時,先確認該欄位是真的混合型別,還是樣本資料有誤導性。
安全使用產生的型別
產生 interface 只是第一步。interface 是編譯期合約 —— 在執行時不會驗證任何東西。response.json() 回傳 any,TypeScript 會讓你把它斷言成 interface,並不會真的檢查。
對於雙方都由你掌控的內部 API,通常沒問題。對外部 API 或使用者提供的資料,要在執行時驗證。建議使用 Zod:
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
active: z.boolean(),
address: z.object({
street: z.string(),
city: z.string(),
zip: z.string(),
}),
tags: z.array(z.string()),
orders: z.array(z.object({
id: z.number(),
total: z.number(),
status: z.string(),
})),
});
type User = z.infer<typeof UserSchema>; // 從 schema 推導的 TypeScript 型別
const raw = await response.json();
const user = UserSchema.parse(raw); // 形狀不符就丟錯Zod 的做法兼顧兩面:schema 是 TypeScript 型別和執行時驗證的唯一真相來源。如果 API 有破壞性變更,你會在資料進入系統的當下立刻知道。
超越 interface:驗證器與型別化用戶端
產生的 interface 是編譯期合約。再進一步是使用一些函式庫,從單一來源同時取得型別和執行時檢查:
- Zod 與 io-ts —— 定義 schema,從中推導 TypeScript 型別,並在邊界驗證傳入資料。如果 API 改變,建置(以及錯誤紀錄)會立即告訴你。
- tRPC 與 型別化的
fetch包裝(例如openapi-fetch)—— 伺服器端的程序或 OpenAPI 定義是真相來源,用戶端自動取得對應的型別,中間沒有 JSON 到 TS 的步驟。 - ts-pattern 與可辨識聯合 —— 一旦你的資料帶有標籤欄位(例如
{ kind: "circle" | "square", ... }),窮盡式模式比對能在編譯時抓出遺漏的情況。
讓型別與 API 保持同步
產生的型別有保存期限。API 會變 —— 欄位被改名、新增必填欄位、或字串欄位變成可為 null。你的 interface 會悄悄地過時。
從 OpenAPI 規格產生,而不是從範例回應
如果 API 有 OpenAPI (Swagger) 定義,openapi-typescript 可以直接從規格產生型別:
npm install -g openapi-typescript
openapi-typescript https://api.example.com/openapi.json -o types/api.ts這比從範例回應產生可靠得多,因為規格會記錄每個欄位,包括 optional、可為 null 變體和列舉值。
在 CI 中重新產生型別
在 CI 流水線中加入一步:從規格或已知正確的樣本重新產生型別,然後執行 tsc --noEmit 檢查是否有壞掉的地方。如果 API 改變,建置會在破壞性變更進入正式環境前就失敗。
常見問題
如何將 JSON 轉換為 TypeScript interface?
把 JSON 貼到像 fixjson 的 JSON 轉 TypeScript 工具 這樣的線上工具立即取得結果;在建置步驟中執行 quicktype;或對長期使用的程式碼,用 openapi-typescript 從 OpenAPI 規格產生。
JSON 型別如何對應到 TypeScript 型別?
字串 → string,所有數字 → number(TypeScript 沒有整數型別),布林 → boolean,物件 → 具名 interface,陣列推斷其元素型別。請見上方的對應表與 JSON 的六種資料型別。
如何處理可為 null 或 optional 的欄位?
鍵可能不存在時用 field?: T;鍵一定存在但值可能為 null 時用 field: T | null。能分析多個樣本的產生器會推斷出這點;單樣本工具則需要手動檢查。
TypeScript interface 會在執行時驗證 JSON 嗎?
不會 —— interface 在編譯時就被擦除。對外部資料若需要執行時安全,請用 Zod 進行驗證,或用 JSON Schema 描述形狀。
結語
將 JSON 轉成 TypeScript interface 是每個 TypeScript 開發者經常要做的事。最適合的方法取決於情境:對於一次性的快速串接,線上轉換器 可讓你不到一分鐘就動工。對於建置流水線,quicktype 或自訂腳本能自動化此流程。對於長期生產程式碼,從 OpenAPI 規格產生型別,並用 Zod 在執行時驗證,提供最強的保證。
無論用哪種方法,產生的 interface 都只是起點 —— 檢查可為 null 的欄位、為非顯而易見的屬性加註解、在業務規則比 JSON 樣本嚴格的地方收緊約束。能準確反映資料的型別,讓整個程式碼庫更易維護。
如果你手上的 JSON 在嘗試轉換之前就有語法錯誤,請先修復它。TypeScript 產生器 —— 跟任何 JSON 解析器一樣 —— 都需要語法上合法的輸入。