JSON 转 TypeScript 接口工具

把一段 JSON 样本翻成 TypeScript 的 interface 定义:逐字段推断类型、把数组元素合并成联合类型、区分 null 与对象、给非法标识符补引号,并挑出超过 2 的 53 次方的整数字段提醒改成字符串。全程在浏览器本地解析,样本不会上传。

uid 字段 9007199254740993 超过 2 的 53 次方,建议后端改成字符串。
类型映射对照表共 20 行,默认显示 12 行。

默认示例里 meta 是 null、nested 是对象、uid 是 64 位 ID,三条各有各的坑。整页不联网、不写本地存储。

使用方法

  1. 打开页面时文本框里已经放好一份示例 JSON,八个字段:id、name、tags、score、active、meta、nested、uid。点「生成接口」,或者在文本框里按 Ctrl / Cmd + Enter,结果立刻出来。
  2. 「根接口名」默认 Root,改成 UserInfo 之类即可。填了以数字开头、含空格或含连字符的名字,会给出指明原因的提示,不会静默替换成别的名字。
  3. 「缩进」三选一:2 空格、4 空格、Tab。改了立刻重算,不用点按钮。
  4. 「未全覆盖的字段标成可选」默认关闭。打开后,出现次数少于样本数的字段会被标成 key?: T。比如 3 个对象里只有 2 个带某字段,可选率正好是 0.666666666667。
  5. 结果区分三层:上面一行是结论(多少个字段、多少个接口),中间是 interface 文本,下面是字段推断表——路径、推断类型、出现次数÷样本数、是否可选、示例值。
  6. 「复制接口」复制整段 interface 文本;「下载 .ts」存成文件,文件名取根接口名。这两个出口都不带字节序标记,TS 源文件加 BOM 会让部分编译器报错。
  7. 样本不是一个对象也认:顶层是数组时按数组元素合并出一个接口,根类型写作 Root[]。
  8. 文本框填了不合法 JSON 时,提示里带上解析器给的位置与原因,比如多余逗号、单引号、注释这类具体问题。

这个JSON转TypeScript接口工具能解决什么问题

第一类是后端还没出类型定义时的前端自保。接口文档只给了一段响应示例,手写 interface 要一个字段一个字段抄,几十个字段的响应抄一遍还要对齐类型:数字该写 number 还是 string、时间是时间戳还是字符串、某字段会不会是 null。把示例粘进来,类型一次推断完,再手工修几处就行。

第二类是把 null 挡在类型之外。JS 里 typeof null 得到 object,照搬会让 null 字段被写成 object,在 strictNullChecks 下编译直接报错。本页把 null 单独归为一档:样本只有 null 时输出 null,样本里 null 与对象混合时输出 T | null,两种写法不一样,页面分开处理。

第三类是数组。手写时最容易只看第一个元素:样本第一个元素是字符串就写 string[],后面夹着数字就漏了。本页把数组里所有元素都扫一遍再合并,异构数组输出 (number | string | boolean)[ ] 这样的联合类型,空数组因为样本不足只给 unknown[]。

第四类是 64 位 ID 的精度墙。示例里的 uid 写成 9007199254740993,JSON.parse 之后就已经变成 9007199254740992,值本身已经错了。本页在扫字段时把这个字段挑出来,在生成的文本里追加注释「建议后端改成字符串」,让人在联调前就发现它。

计算原理与变量说明

整页只有一套规则:递归遍历 JSON 值,把每个路径上的类型并起来,最后按路径生成接口。规则表如下。

样本形态推断结果说明
string / booleanstring / boolean与 TS 同名,直接映射
1 与 95.5numberTS 只有 number,不分 int 与 float
nullnulltypeof null 是 object,但 TS 里单独一档
object内联接口按路径取名,如 RootNested
arrayT[]合并全部元素,不是取第一个
[]unknown[]空数组样本不足,保守写法
整数字段|v| ≥ 9007199254740992提示建议后端改成字符串
字段名/^[A-Za-z_$][A-Za-z0-9_$]*$/不匹配就加单引号

null 与 object 必须分开,这是本页最容易写错的一条。typeof null === 'object' 是 JS 早期实现留下的历史 bug,一直没能修掉,因为它已经被大量线上代码依赖。TypeScript 在 strictNullChecks 打开时不接受把 null 赋给 object,所以照搬 typeof 的结果会直接编译失败。样本只有 null 时输出 null;null 与对象混合时输出 T | null;关掉 strictNullChecks 之后可以简写成对象类型,但那意味着运行时的空值检查全部要靠人自己记着。

数组要合并,不能抽样。同构的 ["tool","web"] 得 string[];异构的 [1,"a",true] 得 (number | string | boolean)[],三个类型谁都不能丢;嵌套的 [[1,2],[3,4]] 得 number[][],第二层继续递归。空数组 [] 里没有任何元素可供观察,写成 never[] 是激进写法(意味着这个数组永远不该有元素),本页取保守的 unknown[]:先承认不知道,等拿到真实数据再收窄。

2 的 53 次方是 JSON 数字的精度墙。Number.MAX_SAFE_INTEGER 是 9007199254740991,再往上就是 9007199254740992,也就是 2 的 53 次方。IEEE 754 双精度只有 53 位有效尾数,超过它之后整数不再连续,9007199254740993 这种写法在 JSON.parse 之后就已经变成 9007199254740992。数据在进入 JS 的那一刻就错了,之后再怎么转类型都救不回来——所以本页的做法是提前点名:扫到绝对值达到 9007199254740992 的整数字段就追加注释,让后端把这个字段改成字符串输出。分布式 ID、订单号、手机号都是这条规则的高发区。

非法标识符加引号,不是改名。判定用的正则是 /^[A-Za-z_$][A-Za-z0-9_$]*$/:含空格的 "user name"、含连字符的 "data-id"、数字开头的 "2fa"、保留字 "class" 都过不了,全部写成 'user name'?: string 这种带单引号的形式。这里不加问号也合法,可选与否只看覆盖率:某字段在 3 个对象里只出现 2 次,可选率就是 0.666666666667,标可选之后调用方必须自己处理它不存在的情况。

JSON转TypeScript接口的嵌套结构按路径取名。Root 下的 nested 对象生成 interface RootNested,字段类型写 RootNested;tags 数组若装的是对象,则生成 interface RootTags,字段类型写 RootTags[]。同一个路径上的所有对象会被合并成一份字段表,出现次数用来判可选。

完整示例(数值仅作演示)

默认样本里 id 是 1、score 是 95.5,两者都映射到 number——TS 不区分整数与浮点,Number.isInteger(95.5) 是 false、Number.isInteger(1) 是 true,但类型层面都是 number。想区分只有自己加注释或者用 branded type。

tags 是 ["tool","web"],两个元素都是字符串,合并后得 string[];如果样本里再夹一个数字,结果立刻变成 (number | string)[],这就是「合并全部元素」与「取第一个元素」的差别。

meta 是 null,输出 meta: null。它不会被写成 object:typeof null 的结果本页不用。nested 是 { "k": 1 },生成独立的 interface RootNested,字段 k 是 number。

uid 写成 9007199254740993,JSON.parse 之后实际是 9007199254740992。本页在这一行后面追加注释「超过 2^53,建议后端改成字符串」,并在提示条里点名 uid。这是整页唯一一条「数据已经错了」的告警。

JSON 值到 TS 类型对照表

第一张表是本页的自测锚点,覆盖空数组、异构数组、保留字与大整数,「复制对照表」复制的就是它。

样本值推断类型说明
"tool"string字符串
1number整数也是 number
95.5numberTS 不区分 int 与 float
trueboolean布尔
nullnulltypeof null 是 object,TS 单独一档
[]unknown[]样本不足,不猜
[1,"a",true](number | string | boolean)[]异构数组合并成联合
[[1,2],[3,4]]number[][]嵌套数组
9007199254740991number安全整数上限
9007199254740993number解析后变成 9007199254740992
"user name"'user name'?: string含空格,加引号
"data-id"'data-id'?: string含连字符,加引号
"2fa"'2fa'?: string数字开头,加引号
"class"'class'?: string保留字,加引号
0.666666666667number3 个样本里出现 2 次的可选率

第二张表是字段名与写法的对照,用来核对加引号的几种情形。

字段名是否合法标识符生成写法
name是name: string;
user name否(含空格)'user name': string;
data-id否(含连字符)'data-id': string;
2fa否(数字开头)'2fa': string;
class否(保留字)'class': string;
$ref是($ 开头合法)$ref: string;

第三张表是「照抄 typeof 会错在哪」,本页的三条纠偏都在里面。

写法照抄 typeof本页输出
meta 为 nullmeta: objectmeta: null
null 与对象混合objectT | null
["a","b"]objectstring[]
[]objectunknown[]
uid 大整数number(已失真)number + 建议改字符串

读法:第一张看类型映射对不对,第二张看引号加得对不对,第三张看三条纠偏有没有生效。

结果边界、舍入与适用说明

常见问题

为什么 null 字段不是 object?

因为 typeof null === 'object' 是 JS 的历史 bug,而 TS 在 strictNullChecks 下不允许把 null 赋给 object。样本只有 null 时输出 null,null 与对象混合时输出 T | null。

空数组为什么是 unknown[] 而不是 any[]?

空数组里没有任何元素可供观察,是「样本不足」而不是「什么都行」。unknown[] 要求你先做类型检查再用,any[] 会关掉检查;never[] 则断言它永远为空,太激进。

为什么 uid 要提示改成字符串?

因为 9007199254740993 超过 2 的 53 次方,JSON.parse 之后就已经变成 9007199254740992,值在进 JS 的那一刻就错了。64 位 ID 只能用字符串承载。

95.5 和 1 都写成 number 吗?

是。TypeScript 只有一个 number,没有 int 与 float 之分。Number.isInteger 能区分运行时值,但类型层面两者都是 number。

字段名里的连字符一定要加引号吗?

要。data-id 不是合法标识符,正则 /^[A-Za-z_$][A-Za-z0-9_$]*$/ 匹配不上,只能写成 'data-id'?: string。含空格、数字开头、保留字同理。

数组里元素类型不一样怎么办?

全部合并成联合类型。[1,"a",true] 得 (number | string | boolean)[],不是只看第一个元素得 string[] 或 number[]。

可选字段是怎么判定的?

看覆盖率:某字段在 3 个对象里只出现 2 次,可选率 0.666666666667,勾选「标成可选」后就加 ?。默认关闭,因为样本少不代表字段真的可选。

样本会上传到服务器吗?

不会。JSON 只在浏览器里解析、遍历、生成文本,没有发起任何网络请求,也没有写入本地存储。

相关工具