Command Palette

Search for a command to run...

JSON 到 TypeScript:从真实 API 数据生成准确的接口

JSON 到 TypeScript:从真实 API 数据生成准确的接口

T
Toolz Team
|Jul 21, 2026|24 最小读数

数据工具 合集的一部分

Bug终于让我停止手写API类型了 小得尴尬,一个支付端点返回 discount: null 对于没有的客户,我已经输入了 discount: number 因为写界面时看到的那个反应刚好是打折的客户,typescript 非常开心,编译器没办法知道 I'd 骗了它,三周后 a .toFixed(2) 在那个领域,正是为那些对我的测试和发票最不重要的用户子集投入了生产。

这就是手动输入 API 的整个问题:您输入的内容 endpoint 返回,编译器尽职尽责地强制执行你的信念而不是现实,TypeScript 给你的下游的每一个保证都只和第一个手写界面一样好,工具链中没有任何东西可以根据实际响应检查静态打字的所有仪式,没有安全性,这可以说比根本没有类型更糟糕 - 至少未打字的代码让你怀疑。

从真实的有效载荷生成类型会颠覆方向,你不是描述你认为的形状是什么,而是采取服务器实际发送的响应,并从中导出形状,输出是机械的:没有乐观,没有你忘记存在的字段,没有 number 数据说在哪里 number | null。我构建[toolz。dev](/并放置基于浏览器的 JSON 到 TypeScript 转换器 这样做,但本指南是关于推理规则本身的 - 生成器可以弄清楚什么,它只能猜测什么,以及您仍然需要思考的地方。

TL;博士: JSON 转换为 TypeScript,请从其值推断出每个键's类型(stringnumberbooleannull(),将嵌套的对象提取到他们自己命名的界面中,并将对象的数组合并到单个元素界面中,其中某些成员缺少的任何密钥都成为可选的。故意决定是否 null 手段 key?: Tkey: T | null-那个选择取决于你的API是省略缺失的字段还是将其发送为null。推理仅反映你提供的样本,所以使用具有若干记录的代表性有效负载,并将输出视为审查的初稿而不是已完成的合同。

为什么从 JSON 生成 TypeScript 类型而不是编写它们?

诚实的答案是,手写类型漂移和生成的类型 don't。当后端添加字段时,您的手写界面会默默地保持错误;没有错误,因为响应中的额外属性对于不't。的类型是不可见的。当后端发生变化时 id 从数字到字符串,您的界面不断坚持它's 数字和 TypeScript 一直一致,直到某些内容连接而不是添加。

There's也是平淡乏味的论证,典型的REST响应有三十个键跨越四个层次的嵌套,转录用手需要十分钟的纯机械工作,而人类执行的机械工作有缺陷率,会打错一个键名,会错过's一个对象数组而不是字符串数组的一个字段,生成器不会。

但最强烈的原因是这一代人创造了形状 可见。将响应粘贴到转换器中,您立即看到您'd在阅读原始JSON时掩盖的事情:那个 metadata 实际上是一个深层嵌套的物体,那个 tags is 有时为空, 你分页列表中有一半的键从一些记录中缺失 生成的界面是对数据的总结's 真实结构, 读取它往往是最快的方式来理解你没有的端点't 写入I've 不止一次将其用作文档步骤在docs是谎言的API上。

这与您的其他数据工具一起适用:如果您're 检查有效负载而不是键入它, json格式化程序 是更好的第一站,如果你'重新比较两个响应,看看版本之间发生了什么变化, JSON 迪夫 直接回答。

JSON 的类型推断实际上是如何工作的?

JSON 有六种值类型,每个 RFC 8259:对象、数组、字符串、数字、 true/false、和 null。TypeScript's原始类型映射到其中四个几乎直接有趣的工作完全在另外两个。

原始语是微不足道的。 字符串值意味着 string.一个数字暗示 number-注意JSON有一个数字类型,所以有's数据中没有信息告诉你是否 1 是整数或浮点数,无论如何,typescript 并不't 区分。 truefalse 暗示 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。与内联版本你'd正在写作 Project['owner'] 到处都是,虽然有效,但读起来很糟糕。

数组是真正决策所在的地方。 数组's类型是其元素类型的并集,因此 [1, 2, 3] 给出 number[][1, "a"] 给出 (number | string)[]。注意第二个括号 - 没有它们, number | string[] 意完全不同的东西(一个数字) 串(字符串的数组),而忘记这个的生成器会发出编译但描述错误事物的代码。

空数组是一个诚实的死胡同。 "tags": [] 告诉您一个键存在并持有数组;它不会告诉您其中包含的内容。正确的输出是 unknown[](declaring to guess)而不是作为完成答案,而应该将其读作生成器拒绝猜测,或者从文档中自己填写,或者找到一个样本,其中数组是't空。

为什么对象数组是合并的而不是联合的?

这是将您'd 使用的生成器与您'd 在五分钟后放弃的生成器分开的单一决定。

考虑记录完全统一的分页响应 - 也就是说,每个真实的分页响应:

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

独立对待每个元素,您将得到两个接口的并集: rows: (Row1 | Row2)[].这是 技术上 样本读数最准确,而且毫无用处。每次访问 row.nickname 现在需要缩小范围,因为 TypeScript 可以'不知道你拥有哪个联合成员。将其扩展到具有多个可选字段的五十个记录响应,并且您将得到数十个几乎相同的接口的联合。没有人想要这个。

有用的解读是这两个对象是一个实体的两个实例,并且 nickname 是格蕾丝没有的领域't有:

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

interface T {
  rows: Row[]
}

That's a merge:收集所有元素中看到的每个密钥,如果它's 缺少任何一个,则标记一个密钥可选。它匹配数据的实际生成方式 - 一个数据库表,一个序列化器,一些可空列 - 它生成无需仪式即可使用的类型。数组元素名称也是单数化的,因此 releases 产量 Release 而不是 Releases的,因为 releases: Releases[] bug一样读起来即使它’'t。

Treatoff是真实的,值得简单地陈述:合并假设数组是同质的。如果你有一个真正的异构数组 - 具有不同形状的事件的馈送,由 a 区分 type 场 - 合并将不同的变体平坦化为一个界面,其中几乎所有内容都是可选的。那's错误的模型,它's一个案例,你应该以生成的输出为起点,手写一个适当的区分并生成器don't知道你的域。这个合并对象和并结合其他一切,这大多数时候是对的,错误的方式你可以立即发现。

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

它们不可互换。在 AretiredAtstring | undefined 而钥匙可能根本不存在于对象上。在 B中,密钥始终存在,其值可能是 null. 下 strictNullChecks- 哪个 类型脚本手册 建议以及您应该携带的内容 - 两者都迫使您处理缺席的情况,但它们强制进行不同的检查,并且序列化方式不同。 JSON.stringify 省略 undefined 完全具有属性并发射 null 对于零,因此选择会一直传播回电线。

API's 的实际行为,正确的答案取决于,没有生成器可以从一个样本中看到:

您的 API's 行为 正确的模型 为什么
那里省略了键's没有值 key?: T 钥匙确实是't在那里;可选是准确的
始终发送密钥, null 当空 key: T | null 钥匙始终存在; ? 会错误地允许缺勤
不一致 - 有时省略,有时为空 key?: T | null 这两种情况都是真实的;模型两者都
发送 null 仅针对错误响应 也不 - 单独对错误进行建模 可取域隐藏响应形状的并集

That last row是值得暂停的,只有在失败情况下才为null的字段是一个信号,表明端点返回两个不同的东西,穿着一个形状,并且修复的是状态字段上的判别并集,而不是可积型生成表面这个模式;它不't解决它。

转换器默认为 key?: T 因为省略时缺失是 JSON API I've 中更常见的约定,并且因为它与上述数组合并(某些记录中缺少的键和某些记录中 's null 采用相同建模的键)更好地组合。关闭选项并 null 留在工会中。这都不是一个技巧;选择你的 API 实际执行的操作。

那些键是't有效的TypeScript标识符呢?

JSON 对象键是任意字符串。TypeScript 属性名称在裸 key: T 位置不是--它们必须是有效的标识符。所以 "content-type""2fa""user.name"、和 "" 是所有合法的 JSON 密钥,不能在界面中不加引号地写入。

修复是引用,它's不是解决方法-引用属性名称是普通的TypeScript:

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

这些属性使用括号表示法访问 (headers["content-type"]),稍微冗长一些,但完全类型安全。注意 class dones't需要引用:保留字完全合法作为 财产名称者,即使它们're作为标识符是非法的,该限制仅适用于typescript期望标识符的地方 - 这就是为什么当同一个单词成为接口时确实需要处理 名字

从此类密钥派生的接口名称比引用需要更多的工作。 2fa 帕斯卡案例到 2fa,它可以't启动一个标识符,所以它得到一个前缀。两个不同的嵌套对象都位于命名的键下 owner 两人都想成为 Owner所以第二个变成了 Owner2。这些都是不光彩的细节,它们'正是决定生成的输出是编译还是需要十五分钟的手修的细节才行,我拿着转换器进行的测试很简单:粘贴任何有效的东西,输出应该编译在下面 strict 没有编辑。

界面或类型别名?

Generator 發射的不是。實際上的差異很窄,但卻是實際的,而且你的編碼庫可能已經有個在其 lint config 中編碼的意見。

interface User {} 支持声明合并 - 两次声明相同的界面名称,TypeScript 将它们组合起来。that's 对于增强您所拥有的库的类型至关重要't 控制,以及其他地方的脚枪,因为两个不相关的同名声明默默合并而不是错误。界面也支持 extends的,当约束失败时,它会产生比交叉类型稍好的错误消息。

type User = {} can't合并,这通常是一个特征,而它's 需要 为任何事物,即't一个对象形状:并集、元组、映射类型、条件类型一个根,即't一个json对象- 一个数字数组,一个裸字符串- 只能表示为别名,所以 type Nums = number[] 无论设置如何,您都会得到什么。

对于生成的 API 类型,我倾向于 interface的,(主要是因为错误消息稍微好一些),而且因为合并风险是理论上的,当每个名字都生活在一个生成的文件中。但这接近于抛硬币,并且与周围代码的一致性比优点更重要。如果您的ESLint配置有 @typescript-eslint/consistent-type-definitions 无论哪种方式设置,匹配它并停止思考它。

这与 JSON 模式和 TypeScript 有何不同?

这些解决了真正不同的问题,它'值得精确,因为"JSON 到 TypeScript"和"JSON 模式到 TypeScript"是一个单词,经常被混淆。

JSON 到 TypeScript 是从示例推断出来的。 Input: a value。generator 观察那里的 what's 并进行概括,它无法知道是否需要一个字段,字符串是否被约束到枚举,一个数字是否有最小值,或者你粘贴的一个样本是否代表它's 从单个观察归纳,具有所有暗示。

JSON 模式到 TypeScript 是从声明的翻译。 输入:a JSON 架构 文档,其中已经说明了类型, required 数组、枚举、格式和约束。生成器是't猜测-it's将现有合约音译为typescript语法。 required 映射到非可选属性;一个 enum 映射到字符串字面并集; oneOf 映射到联合类型。

规则直接遵循: 如果存在模式,请使用它。 JSON 模式、OpenAPI 规范、a .proto file,或者一个graphql模式在一种采样响应永远不会存在的方式上是权威的推断是你在没有模式存在时所达到的 - 一个未记录的内部端点,一个docs陈旧的第三方API,一个有机增长的配置文件格式,一个固定装置你're编写测试反对公平地说,它描述了我们任何人都实际处理的JSON的很大一部分。

There's一条值得一提的中间路径:使用推理到 引导程序(hand),然后用手维护。从真实响应生成界面,得到形状和字段名称正确,然后编辑它 - 收紧a string 到一个字面上的并集,您知道允许的值,请修复一个 unknown[] 样本留空,将合并的接口分割成适当的判别并集。生成器执行机械 90%,并且您应用它在结构上无法拥有的域知识。

推理在哪里出错?

1个简短、诚实的列表,这些每一个都是方法的限制,而不是某个特定工具中的一个bug,而知道它们是很好地使用生成的类型和被它们烧毁之间的区别。

单个样本无法确定类型。 一个领域's number 在你的样本中可能是 null 5%的记录中。你粘贴的所有三个记录中都存在的's字段可能是整个数据集中的可选。推理报告它所看到的。粘贴更多记录 - 理想情况下是结果的真实页面,而不是一个手工挑选的对象 - 并且选项变得有意义地更准确。

字符串隐藏了它们的真实类型。 ISO时间戳、UUID、网址、电子邮件地址都只是 string 到 JSON 解析器。 "2026-07-16T09:00:00Z" 是语义上的日期;数据中没有任何内容是这样说的。如果您的代码库有一个品牌 ISODateString type,你're用手代入。

数字失去了精确的区别。 JSON's单数类型意味着一个ID's服务器上的64位整数作为JavaScript数字到达,并且可能在您的生成器看到它之前已经失去了精度- Number.MAX_SAFE_INTEGER 9×10¹5左右,而twitter以艰难的方式学到了这一点,如果你的api发送大整数作为字符串,那's why,以及生成的 string 是正确的。

字面价值看起来像它们的一般类型。 "status": "active" 推断 string不是 "active" | "archived" | "pending"。较窄的类型更有用,没有样本可以证明,这是我对生成的输出所做的最常见的手工编辑。

空容器什么也没说。 [] 给出 unknown[]{} gives一个空的界面。两者都是生成器诚实。

这些都不会使推理变得不安全--它使推理变得不安全 草案。工作的工作流程是:生成,仔细读取输出,修复你知道的样本不能't say, commit。that's的四五件事,仍然比用手转录三十个键快一个数量级,更准确,这是实际的替代方案。

我的 JSON 是否在任何地方上传?

不,这是一类工具,问题应该得到真正的答案,而不是徽章。

JSON you'd中想想什么's粘贴到类型生成器中,它's是一个API响应,这意味着它似乎包含一个承载令牌,一个会话标识符,一个客户电子邮件,一个内部用户ID,一个定价层,一个网络钩秘密那个's不是假设的- 它's的模态情况,因为整点就是你抓了一个 真实的 对类型的响应反对。

任何服务器端转换器都必然会接收该有效负载。它可能不会记录它,而且它可能不会记录它't,但您're 扩展信任您不't 必须扩展,并且根据数据,您可能会为根本没有业务接触网络的任务创建合规性问题。

Type推断是对解析值进行纯计算,它不需要网络,不需要帐户,不需要存储,toolz。dev上的转换器是几百行在你的选项卡中运行的无依赖TypeScript;有效负载是浏览器中的JavaScript字符串's内存,它会保留在那里,你可以按照你'd验证任何此类声明的方式验证这一点-打开网络选项卡并点击生成,或者关闭你的wifi并观看它继续工作这是站点上每个工具背后的相同原理,I've写了为什么它更广泛地重要 为什么基于浏览器的工具在敏感数据方面优于服务器端工具

一个有效的例子

Here's工具附带的样本,这是故意构建的,以执行上述每一条规则:

{
  "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 2名成员合併为一 Release- 单数 - 和 notes 成为可选的,因为第二个版本没有't有一个。 retiredAt 成为可选的,因为它唯一观察到的值为零。

现在阅读您'd修复的内容。 retiredAt?: null is 生成器's诚实的报道,它从未见过非空值,它's作为一种类型毫无用处- you'd将其更改为 retiredAt?: string 因为你知道它's存在时的时间戳。该单次编辑是整个课程:生成器在一个粘贴中获得了七键结构、嵌套、数组合并和可选性,并留下了一个决定,要求你知道字段的含义。

常问问题

如何将 JSON 转换为 TypeScript 接口?

JSON粘贴到转换器中,将根类型名称设置为任何资源调用,然后按生成。它推断每个密钥的类型,将嵌套对象拉出到他们自己的命名界面中,将对象数组合并到单个元素类型中,并输出可以直接复制到一个中的代码 .ts file。there's没有注册,也没有上传 - 推理在你的浏览器中运行。

对象数组会发生什么?

They're合并到一个描述单个元素的界面中,属性被键入为它的数组,任何出现在某些数组成员中但不出现在其他成员中的键都成为可选的,这与真实分页数据的行为方式相匹配,其中记录来自一个表,某些列可否处理,它处理不当的一个情况是不同事件类型的真正异构数组,您应该将其手工转换为受区分的并集。

null 是应该成为可选键还是与 null 结合?

API 是否省略了缺失的字段或者发送为 null 即可,如果省略了, key?: T 是准确的。如果密钥始终存在,有时是空的, key: T | null 是准确的,并且使用 ? would错误地允许键丢失,转换器默认为可选,让你切换,因为两者串行不同 - JSON.stringify 删除未定义的属性,但发出空属性。

它可以从单个 JSON 样本中推断出准确的类型吗?

它推断出准确的类型 对于那个样本的,这与 't 是一样的。's 在您的一条记录中的数字在其他记录中可能为空的字段;所有三个记录中存在的字段可能在整个数据集中是可选的。使用具有多个记录的代表性有效负载,而不是一个手工挑选的对象,并将输出视为审查草案而不是完成的合同。

JSON 到 TypeScript 和 JSON 模式到 TypeScript 之间的区别是什么?

该工具从示例值推断类型; JSON Schema 到 TypeScript 翻译一个已经声明类型、必填字段和枚举的形式模式。模式是权威的,推断是猜测,因此,如果您有 JSON Schema、OpenAPI 规范或 GraphQL 模式,请使用推断。对于不存在模式且您所拥有的只是响应体的非常常见的情况。

它如何处理 aren't 有效标识符的密钥?

输出中引用带有破折号、点、空格或前导数字的键,因此 "content-type" 变成 "content-type": string。that's 有效的 TypeScript,以括号表示法访问。保留的单词如 class don't需要引用作为属性名称,从此类键派生的接口名称是pascalcased和前缀,如果它们'd以数字开头,并且冲突的名称得到一个数字后缀,因此输出总是编译。

我应该生成接口或类型别名吗?

Match 你的代码库已经做了什么- 这多是一个一致性问题,接口支持声明合并和 extends的,并且给出稍微清晰的错误消息类型别名可以't合并,这通常是可取的,并且对于任何是't对象形状的根's数组或原语都以别名方式发出,因为那里's没有对象声明接口为。

我的json是否上传到服务器?

No。整个推理引擎在你的浏览器中以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!