Command Palette

Search for a command to run...

JSON 到 TypeScript:從真實的 API 資料產生準確的介面

JSON 到 TypeScript:從真實的 API 資料產生準確的介面

T
Toolz Team
|Jul 21, 2026|24 閱讀

數據工具 合集的一部分

最終讓我停止手寫 API 類型的錯誤非常小。返回付款端點 discount: null 對於沒有的顧客,我已經輸入了 discount: number 因為我在編寫介面時看到的一個回應恰好是一位有折扣的客戶。 TypeScript 非常高興。編譯器無法知道 I'對此撒了謊。三週後a .toFixed(2) 在該領域,為對我的測試最不重要、對發票最重要的用戶子集進行了生產。

這就是手動輸入 API 的全部問題:您輸入內容 相信 端點返回,編譯器盡職盡責地強制執行您的信念而不是現實。 TypeScript 為您提供的每個下游保證都只與第一個手寫介面一樣好,並且工具鏈中沒有任何內容可以根據實際回應進行檢查。您無需任何安全性即可完成所有靜態打字儀式,這可以說比根本沒有類型更糟糕 - 至少未分型的程式碼會讓您產生懷疑。

從真實有效負載產生類型會翻轉方向。您不是描述您認為的形狀是什麼,而是採用伺服器實際發送的回應並從中導出形狀。輸出是機械的:沒有樂觀,沒有你忘記存在的字段,沒有 number 數據顯示的地方 number | null。 我建立 [toolz。dev](/並放置一個基於瀏覽器的 JSON 到 TypeScript 轉換器 這樣做,但本指南是關於推理規則本身的 - 生成器可以弄清楚什麼,它只能猜測什麼,以及您仍然需要思考的地方。

TL;DR: 若要將 JSON 轉換為 TypeScript,請從其值推斷每個鍵's 類型 (string, number, boolean, null),將嵌套物件提取到自己的命名介面中,並將物件陣列合併到單一元素介面中,其中某些成員缺少的任何金鑰都變成可選的。故意決定是否 null 手段 key?: T 或者 key: T | null- 此選擇取決於您的 API 是否省略缺失欄位或將它們發送為空。推理僅反映您提供的樣本,因此使用具有多個記錄的代表性有效負載,並將輸出視為經過審查的初稿而不是完成的合約。

為什麼要從 JSON 產生 TypeScript 類型而不是編寫它們?

誠實的答案是手寫類型漂移並產生類型 don't。當後端新增欄位時,您的手寫介面會默默地保持錯誤;沒有任何錯誤,因為回應中的額外屬性對於沒有'的類型來說是不可見的。不提它們。當後端改變時 id 從數字到字串,您的介面不斷堅持它'一個數字和 TypeScript 一直一致,直到某些東西連接而不是添加。

There'還有簡單的單調乏味的論點。典型的 REST 回應在四個巢狀層級上有 30 個鍵。手工轉錄需要十分鐘的純機械功,而人類執行的機械功有缺陷率。您將輸入一個鍵名。您將錯過一個欄位 's 物件數組而不是字串數組。生成器不會。

但最強烈的原因是世代創造了形狀 可見。 將回應貼到轉換器中,您會立即看到您所看到的內容'會掩蓋閱讀原始 JSON:那個 metadata 實際上是一個深嵌套的物件 tags 有時是空的,某些記錄中缺少分頁清單中的一半鍵。產生的介面是資料摘要'真實結構,閱讀它通常是理解您沒有的端點的最快方法't 寫。 I'在文件是謊言的 API 上多次將其用作文件步驟。

如果這與其他資料工具相符:如果您'正在檢查有效負載而不是輸入它,則 json 格式化程式 是更好的第一站,如果你'正在比較兩個回應以查看版本之間發生了什麼變化 JSON 差異 直接回答。

JSON 的類型推論實際上如何運作?

JSON 有六種值類型 RFC 8259:物件、數組、字串、數字、 true/false, 和 null. TypeScript'原始類型幾乎直接映射到其中四種。有趣的工作完全在於其他兩個。

原始是微不足道的。 字串值意味著 string. 數字意味著 number- 請注意,JSON 有一種數字類型,因此 #39;數據中沒有資訊告訴您是否 1 是整數或浮點數,typescript 不區分 't 區分。 true 或者 false 暗示 boolean. 這部分沒有歧義。

對象變成介面。 每個物件值都成為一個命名接口,它出現的鍵提供了名稱,轉換為 PascalCase。一個鑰匙 owner 產生 interface Owner。 嵌套遞歸:物件內部的物件會產生第一個引用的第二個介面。這比聽起來更重要。替代方案 - 匿名內聯每個嵌套形狀 - 產生一個不可讀的聲明,並且不會給您任何重要的內容:

// Inlined: technically correct, practically useless
interface Project {
  owner: { id: number; email: string; twoFactor: boolean }
}

// Extracted: you can import and reference Owner on its own
interface Project {
  owner: Owner
}

interface Owner {
  id: number
  email: string
  twoFactor: boolean
}

一次 Owner 以名稱形式存在,可以鍵入僅採用所有者的函數 (owner: Owner) => void. 使用內聯版本 you'會寫作 Project['owner'] 到處都是,這有效,但讀起來很糟糕。

數組是真正決策所在的地方。 數組's 類型是其元素類型的並集,因此 [1, 2, 3]number[][1, "a"](number | string)[]. 請注意第二個括號 - 不帶括號, number | string[] 意思是完全不同的東西(數字) 或者 字串數組)和忘記此內容的生成器發出編譯但描述錯誤內容的程式碼。

空數組是一個誠實的死胡同。 "tags": [] 告訴你一個鑰匙存在並且保存一個數組;它不會告訴您其中發生了什麼。正確的輸出是 unknown[],你應該將其讀作生成器拒絕猜測,而不是作為完成的答案。從文件中自行填寫,或找到數組為 't 空的範例。

為什麼物件數組合併而不是聯合?

這是將發電機與發電機分開的單一決定,you'd 使用,you'd 使用,五分鐘後放棄。

考慮一個分頁回應,其中記錄是 't 完全統一 - 也就是說,每個真實的分頁回應:

{
  "rows": [
    { "id": 1, "name": "Ada", "nickname": "The Countess" },
    { "id": 2, "name": "Grace" }
  ]
}

獨立處理每個元素,您會得到兩個介面的並集: rows: (Row1 | Row2)[]. 這是 技術上 最準確的樣本讀數,毫無用處。每次訪問 row.nickname 現在需要縮小範圍,因為 TypeScript 可以'不知道你有哪個工會成員。將其擴展到具有多個可選字段的 50 個記錄回應,並且您將獲得數十個幾乎相同的介面的聯合。沒有人想要這樣。

有用的解讀是這兩個物件是一個實體的兩個實例,並且 nickname 是格蕾絲沒有的領域'有:

interface Row {
  id: number
  name: string
  nickname?: string
}

interface T {
  rows: Row[]
}

That's 合併:收集所有元素中看到的每個鍵,並標記一個可選鍵(如果它's)。其中任何一個都不存在。它與資料的實際生成方式相匹配 - 一個資料庫表、一個序列器、一些可歸零的列 - 並且它產生您可以無需儀式即可使用的類型。數組元素名稱也是單數化的,因此 releases 產量 Release 而不是 Releases,因為 releases: Releases[] 即使它是 't,讀起來也像個錯誤。

權衡是真實的,值得簡單說明:合併假設數組是齊次的。如果您有一個真正的異質數組 - 不同形狀的事件的來源,由 a 區分 type 欄位 - 將不同的變體合併到一個介面中,其中幾乎所有內容都是可選的。那'是錯誤的模型,它'這是一個案例,您應該以生成的輸出為起點,手寫一個適當的有區別的並集。生成器 don'不知道你的域。這個合併了物件並將其他所有內容聯合起來,這在大多數情況下是正確的,但在你可以立即發現的方式上是錯誤的。

null 應該成為可選金鑰還是工會成員?

這兩種約定都是可以防禦的,而且差異會很嚴重,因此請有目的地做出決定,而不是接受您的工具預設的任何內容。

給定 { "retiredAt": null }() 有兩個讀數:

interface A { retiredAt?: string }      // the field may be absent
interface B { retiredAt: string | null } // the field is present and may be null

它們不可互換。在 A, retiredAtstring | undefined 並且該物件上可能根本不存在該鍵。在 B,密鑰始終存在,其值可能為 null. 下 strictNullChecks- 哪個 TypeScript 手冊 建議以及您應該穿的 - 兩者都迫使您處理缺勤的情況,但它們強制不同的檢查並且它們的序列化不同。 JSON.stringify 省略 undefined 完全屬性並發出 null 對於空的,因此選擇權會一直傳播回電線。

正確答案取決於 API' 的實際行為,任何生成器都無法從一個樣本中看到:

您的 API's 行為 正確的模型 為什麼
當 ' 沒有價值時省略鑰匙 key?: T 關鍵確實是't在那裡;可選是準確的
始終發送密鑰, null 空的時候 key: T | null 鑰匙始終存在; ? 會錯誤地允許缺席
不一致 - 有時省略,有時為空 key?: T | null 兩種情況都是真實的;兩者都建模
發送 null 僅針對錯誤回應 兩者都不 - 單獨對錯誤進行建模 可歸零欄位隱藏響應形狀的並集

最後一行是值得暫停的。僅在故障情況下為零的欄位是端點傳回兩個不同的事物並穿著一種形狀的信號,並且修復是狀態欄位上的判別並集,而不是可無效屬性。類型生成浮出水面這個模式;它沒有't 解決它。

轉換器預設為 key?: T 因為當不存在時省略是 JSON API I&#39 中更常見的約定;ve 使用過,並且因為它在上述數組合併中組合得更好(某些記錄中缺少一個鍵,並且某些記錄中的 's null 鍵以相同的方式建模)。關閉選項並 null 相反,留在工會中。這都不是技巧;選擇您的 API 實際執行的一個。

那麼有效的 TypeScript 識別碼為 39 的金鑰呢?

JSON 物件鍵是任意字串。裸中的 TypeScript 屬性名稱 key: T 位置不是 - 它們必須是有效的標識符。所以 "content-type", "2fa", "user.name", 和 "" 都是合法的 JSON 金鑰,不能在介面中不加引號地編寫。

修復方法是引用,它'不是解決方法 - 引用的屬性名稱是普通的 TypeScript:

interface Headers {
  "content-type": string
  "2fa": boolean
  class: string
}

這些屬性透過括號表示法 (()) 進行存取headers["content-type"]),稍微冗長但完全類型安全。注意 class 不'不需要引用:保留詞是完全合法的 屬性名稱,即使他們'作為標識符是非法的。此限制僅適用於 TypeScript 期望標識符的情況 - 這就是為什麼同一個字在成為介面時確實需要處理 名字.

源自此類金鑰的介面名稱需要比引用更多的工作。 2fa 帕斯卡案例到 2fa,這可以't啟動一個標識符,所以它得到一個前綴。兩個不同的嵌套物件都在命名的鍵下 owner 雙方都想成為 Owner,所以第二個就變成了 Owner2.這些都是乏味的細節,它們'正是決定產生的輸出是否編譯或需要十五分鐘的手工修復才能編譯的細節。我將轉換器進行的測試很簡單:貼上任何有效的內容,輸出應編譯在下面 strict 無需編輯。

介面或類型別名?

生成器發出任一。實際差異很窄但真實,您的程式碼庫可能已經在其 lint 配置中編碼了意見。

interface User {} 支援聲明合併 - 聲明相同的介面名稱兩次,TypeScript 將它們組合起來。那'對於從您擁有的庫中增強類型至關重要't 控制,以及其他地方的腳槍,因為兩個不相關的同名聲明會默默合併而不是出錯。介面也支援 extends,當約束失敗時,它比交集類型產生稍微更好的錯誤訊息。

type User = {} can't 合併(這通常是一個功能)和 it's 必需的 對於任何不是 't 物件形狀的東西:並集、元組、映射類型、條件類型。 JSON 物件的根 - 數字數組、裸字串 - 只能表示為別名,因此 type Nums = number[] 無論設定如何,您都會得到什麼。

對於生成的 API 類型,我傾向於 interface,主要是因為錯誤訊息稍好一些,而且當每個名稱都存在於一個生成的檔案中時,合併風險是理論上的。但這接近拋硬幣,與周圍程式碼的一致性比優點更重要。如果您的 ESLint 配置有 @typescript-eslint/consistent-type-definitions 無論哪種方式,都要匹配它,然後停止思考它。

從 JSON Schema 到 TypeScript 有何不同?

這些解決了真正不同的問題,並且 #39;值得精確,因為 "JSON 到 TypeScript"和&引用;JSON 模式到 TypeScript"相隔一個字並且經常混淆。

JSON 到 TypeScript 是從範例推斷出來的。 輸入:一個值。生成器觀察什麼'在那裡並進行概括。它無法知道是否需要一個欄位、字串是否被限制為枚舉、數字是否具有最小值、或者您貼上的一個樣本是否具有代表性。它'從單一觀察中歸納,其中包含所暗示的一切。

JSON Schema 到 TypeScript 是從聲明翻譯而來的。 輸入:a JSON 模式 文檔,已經說明類型, required 數組、枚舉、格式和約束。生成器是't猜測-it'將現有合約音譯為 TypeScript 語法。 required 映射到非可選屬性;一個 enum 映射到字串文字聯合; oneOf 映射到聯合類型。

規則直接如下: 如果存在模式,請使用它。 JSON 模式、OpenAPI 規範、a .proto 文件或 GraphQL 模式具有權威性,而採樣回應卻從未如此。推理是當不存在模式時您所達到的目標 - 未記錄的內部端點、文件陳舊的第三方 API、有機增長的配置文件格式、固定裝置 you'正在編寫測試。公平地說,它描述了我們任何人實際處理的 JSON 的很大一部分。

There'是一條值得一提的中間路徑:使用推理 引導程式,然後用手維護。從真實響應生成介面以獲得正確的形狀和字段名稱,然後對其進行編輯 - 收緊 a string 對於您知道允許的值的文字聯合,請修復一個 unknown[] 樣本留空,將合併的介面分割成適當的判別聯合。生成器執行 90% 的機械操作,並且應用其結構上無法擁有的領域知識。

推論哪裡會出錯?

一個簡短、誠實的清單。其中每一個都是方法的限制,而不是特定工具中的錯誤,了解它們是很好地使用生成的類型和被它們燒毀之間的區別。

單一樣品未充分確定類型。 's 的欄位 number 在你的樣本中可能是 null 5% 的記錄。 '存在於您貼上的所有三筆記錄中的欄位可能是整個資料集中的可選內容。推理報告它所看到的內容。貼上更多記錄 - 理想情況下是真實的結果頁面而不是手工挑選的對象 - 並且選項變得更加準確。

字串隱藏其真實類型。 ISO 時間戳、UUID、URL 和電子郵件地址都只是 string 到 JSON 解析器。 "2026-07-16T09:00:00Z" 語意上是日期;數據中沒有任何內容說明這一點。如果您的程式碼庫有一個品牌 ISODateString 輸入,you'用手替換它。

數字失去了精確度差異。 JSON'單一數字類型意味著 ID '伺服器上的 64 位元整數以 JavaScript 編號的形式到達,並且在生成器看到它之前可能已經失去了精度 - Number.MAX_SAFE_INTEGER 大約是 9×1015,Twitter 透過艱苦的方式學會了這一點。如果您的 API 將大整數作為字串發送,則 's 為什麼,以及生成的 string 是正確的。

文字值看起來像它們的一般類型。 "status": "active" 推論 string,不 "active" | "archived" | "pending". 窄型比較有用,沒有樣品可以證明。這是我對產生的輸出進行的最常見的手動編輯。

空容器什麼也沒說。 []unknown[]{} 給出一個空介面。老實說,兩者都是生成器。

所有這些都不會使推論變得不安全--它使推論變得不安全 草稿. 工作流程是:產生、仔細閱讀輸出、修復您知道的樣本無法做的四到五件事't 說,提交。那'仍然比手動轉錄 30 個鍵更快、更準確,而手動轉錄 30 個鍵是實際的選擇。

我的 JSON 上傳到哪裡了嗎?

不,這是一類工具,問題值得真正的答案,而不是徽章。

想想什麼'在 JSON 中,you'd 貼到類型產生器中。它'是 API 回應,這意味著它可能包含承載令牌、會話識別碼、客戶電子郵件、內部使用者 ID、定價層、網路掛鉤秘密。 That'不是假設的 - it'模態情況,因為重點是你抓住了一個 真正的 對類型的回應反對。

任何伺服器端轉換器都必須接收該有效負載。它可能不會記錄它,而且它可能不會't,但是你'正在擴展信任,你不知道'不必擴展,並且根據數據,您可能會為根本不涉及網路的任務創建合規性問題。

類型推斷是對解析值的純粹計算。它不需要網路、帳戶、儲存。 toolz。dev 上的轉換器是選項卡中運行的數百行無依賴性的 TypeScript;有效負載是瀏覽器中的 JavaScript 字串'記憶體並保留在那裡。您可以按照 'd 驗證任何此類聲明 - 開啟網路標籤並點擊生成,或關閉 wifi 並觀看其繼續工作。這是網站上每個工具背後的相同原則,I'已經寫過為什麼它更廣泛地重要 為什麼基於瀏覽器的工具在敏感資料方面擊敗了伺服器端工具.

一個可行的例子

這裡'是工具附帶的樣本,它是專門為執行上述所有規則而構建的:

{
  "id": 4821,
  "name": "Toolz",
  "isPublic": true,
  "retiredAt": null,
  "owner": {
    "id": 12,
    "email": "[email protected]",
    "twoFactor": false
  },
  "tags": ["developer", "privacy", "browser"],
  "releases": [
    { "version": "1.0.0", "downloads": 1420, "notes": "First cut" },
    { "version": "1.1.0", "downloads": 3310 }
  ]
}

帶有根名 Project,這產生:

export interface Project {
  id: number
  name: string
  isPublic: boolean
  retiredAt?: null
  owner: Owner
  tags: string[]
  releases: Release[]
}

export interface Owner {
  id: number
  email: string
  twoFactor: boolean
}

export interface Release {
  version: string
  downloads: number
  notes?: string
}

閱讀發生的事情。 owner 被提取到自己的介面中並透過名稱引用。 tags 崩潰了 string[] 因為每個元素都是一個字串。 releases 將其兩名成員合併為一名 Release- 單數 - 和 notes 成為可選,因為第二個版本沒有'沒有。 retiredAt 成為可選的,因為它唯一觀察到的值為空。

現在閱讀您的內容#39;d 修復。 retiredAt?: null 是生成器'誠實的報告,它從未見過非空值,並且它'作為類型毫無用處 - you'd 將其更改為 retiredAt?: string 因為你知道它'存在時有一個時間戳記。這個單一編輯就是整個課程:生成器將七鍵結構、嵌套、數組合併和可選性直接粘貼在一個粘貼中,並為您留下一個需要了解該字段含義的決定。

問號

如何將 JSON 轉換為 TypeScript 介面?

將 JSON 貼到轉換器中,將 root 類型名稱設定為任何呼叫的資源,然後按 Generate。它推斷每個鍵的類型,將嵌套物件拉出到它們自己的命名介面中,將物件陣列合併到單一元素類型中,並輸出可以直接複製到 a 中的程式碼 .ts 文件。那裡'沒有註冊,也沒有上傳 - 推論在您的瀏覽器中運行。

物件陣列會發生什麼事?

他們'合併到一個描述單一元素的介面中,並且該屬性被鍵入為其數組。出現在某些數組成員中但不出現在其他數組成員中的任何鍵都變成可選的。這與真實分頁資料的行為方式相符,其中記錄來自一個表,並且某些列可為空。它處理不當的一種情況是不同事件類型的真正異質數組,您應該將其手動轉換為受歧視的並集。

null 應該成為可選鍵還是與 null 的並集?

這取決於您的 API 是省略缺少的欄位還是將它們發送為 null。如果它省略了它們, key?: T 是準確的。如果密鑰始終存在且有時為空,則 key: T | null 準確,使用 ? 會錯誤地允許金鑰遺失。轉換器預設為可選並允許您切換,因為兩者的序列化方式不同 - JSON.stringify 丟棄未定義的屬性,但發出空屬性。

它可以從單一 JSON 樣本推斷出準確的類型嗎?

它推斷出準確的類型 對於那個樣本,這是't同一件事。一個欄位 's 您的一筆記錄中的一個數字在其他記錄中可能為空;您貼上的所有三筆記錄中都存在的欄位可能是整個資料集中的可選欄位。使用具有多個記錄而不是一個精心挑選的物件的代表性有效負載,並將輸出視為經過審查的草稿而不是完成的合約。

什麼' JSON 到 TypeScript 以及 JSON Schema 到 TypeScript 之間的差異?

該工具從範例值推斷類型; JSON Schema 到 TypeScript 翻譯了一個已經聲明類型、所需欄位和枚舉的形式模式。該模式具有權威性,推論是一種猜測,因此如果您有 JSON 模式、OpenAPI 規範或 GraphQL 模式,請使用它。推論適用於不存在模式且您擁有的只是回應主體的非常常見的情況。

它如何處理有效標識符為 't 的金鑰?

輸出中引用了帶有破折號、點、空格或前導數字的鍵,因此 "content-type" 成為 "content-type": string. That's 有效的 TypeScript,使用括號表示法存取。保留詞如 class don'不需要引用作為屬性名稱。從此類鍵派生的介面名稱如果'd 以數字開頭,則帶有 PascalCased 和前綴,並且衝突的名稱會獲得數字後綴,因此輸出始終編譯。

我應該產生介面還是輸入別名?

無論您的程式碼庫已經做什麼,都匹配 - 這主要是一個一致性問題。介面支援聲明合併和 extends,並給出稍微更清晰的錯誤訊息。輸入別名 can't 合併,這通常是理想的,並且是任何 't 物件形狀的東西所必需的。根 's 數組或原語以任意方式作為別名發出,因為那裡'沒有物件來聲明介面。

我的 JSON 上傳到伺服器了嗎?

不。整個推理引擎在瀏覽器中以 JavaScript 形式運行,沒有網路呼叫、沒有日誌記錄、沒有儲存。這裡比大多數工具更重要,因為 JSON you'd 貼到類型產生器中通常是真實的 API 回應,其中包含令牌、客戶記錄或內部 ID。在產生時開啟網路選項卡,或與網際網路斷開連線 - 它繼續運作。


相關工具: json 格式化程式 為了先檢查有效負載, JSON 到 YAMLjson 到 xml 用於格式轉換,以及 JSON 差異 用於發現兩個回應之間發生了什麼變化。進一步閱讀: JSON 工具的終極指南開發人員's 編碼工具指南.

Frequently Asked Questions

Paste your JSON into the converter, set the root type name to whatever the resource is called, and press Generate. It infers the type of every key, pulls nested objects out into their own named interfaces, merges arrays of objects into a single element type, and outputs code you can copy straight into a .ts file. There's no signup and no upload — the inference runs in your browser.

Comments

0 comments

0/2000 characters

No comments yet. Be the first to share your thoughts!