JSON 转 TypeScript 类型推断引擎:递归、数组合并与命名
JSON 转 TypeScript 不是把当前值的 typeof 逐个打印出来,而是递归处理对象、数组和混合样本。数组需要合并元素类型,字段名需要生成合法且稳定的类型键;单个样本仍无法证明字段必有、范围完整或未来不会出现其他形状。
一、问题概述:样本值只是类型证据的一部分
[{"id":1},{"id":2,"name":"Ada"}] 需要把数组元素合并成一个对象类型,并将只在部分元素出现的 name 标为可选。直接复制第一项会丢失字段,直接生成 union 也可能让调用方无法使用共同属性。
二、最小复现:递归进入对象和数组
下面的推断器只针对 JSON 值,不处理 Date、Map 或 class 实例;它将空数组标为 unknown[],避免凭空猜测元素类型。
function inferJson(value: unknown): string {
if (value === null) return "null";
if (typeof value === "string") return "string";
if (typeof value === "number") return "number";
if (typeof value === "boolean") return "boolean";
if (Array.isArray(value)) return value.length === 0 ? "unknown[]" : "(" + mergeTypes(value.map(inferJson)) + ")[]";
if (typeof value === "object") return inferObject([value as Record<string, unknown>]);
return "unknown";
}
console.log(inferJson({ id: 1, flags: [true, false] })); // { id: number; flags: (boolean)[] }三、根因:数组合并需要共同字段与可选性
对象数组的元素不一定拥有相同键集合。推断器需要收集所有键、合并同名值的类型,并把缺失键标记为 optional;冲突标量则生成 union,而不是随意选择第一种类型。递归深度和循环引用也必须有边界。
四、推荐方案:先生成保守类型,再由开发者确认命名
字段名应保留原始语义;不是合法 TypeScript 标识符的键可使用字符串字面量键或经过记录的重命名规则。对空数组、null、混合数组和深层对象输出提示,不要把样本推断宣称为完整 schema。
五、完整代码:合并对象数组并生成类型文本
以下示例实现递归对象推断、数组对象合并、缺失字段 optional 和基本 union。它是可读的最小引擎,不负责命名冲突、循环引用或运行时校验。
function mergeTypes(types: string[]): string {
const unique = [...new Set(types)];
return unique.length === 1 ? unique[0] : unique.sort().join(" | ");
}
function inferObject(objects: Array<Record<string, unknown>>): string {
const keys = [...new Set(objects.flatMap((object) => Object.keys(object)))].sort();
const fields = keys.map((key) => {
const present = objects.filter((object) => Object.prototype.hasOwnProperty.call(object, key));
const type = mergeTypes(present.map((object) => inferJson(object[key])));
const optional = present.length < objects.length ? "?" : "";
return JSON.stringify(key) + optional + ": " + type;
});
return "{ " + fields.join("; ") + " }";
}
function inferJson(value: unknown): string {
if (value === null) return "null";
if (typeof value === "string") return "string";
if (typeof value === "number") return "number";
if (typeof value === "boolean") return "boolean";
if (Array.isArray(value)) {
if (value.length === 0) return "unknown[]";
if (value.every((item) => item && typeof item === "object" && !Array.isArray(item))) return "(" + inferObject(value as Array<Record<string, unknown>>) + ")[]";
return "(" + mergeTypes(value.map(inferJson)) + ")[]";
}
if (typeof value === "object") return inferObject([value as Record<string, unknown>]);
return "unknown";
}
console.log(inferJson([{ id: 1 }, { id: 2, name: "Ada" }]));
// ({ "id": number; "name"?: string })[]六、常见错误方案
把每个数字细分成整数或浮点并不能推断业务范围;把所有数组元素取第一项会漏掉后续字段;把非法键名直接拼进 interface 会生成不可编译的代码。只输出类型文本而不提示样本局限,也会让开发者误以为完成了 schema 设计。
七、边界条件:null、混合数组与命名
数组可能混合 null、标量和对象,合并结果应保守地使用 union;全是 null 的字段没有非空类型证据。保留原键名、字符串键和重命名映射要有明确规则,递归深度和超大样本应设置限制。
八、如何验证生成类型
用多元素对象数组、缺失键、null、空数组、混合标量和包含空格/连字符的键名测试。将生成文本交给 TypeScript 编译器,再用额外样本检查是否出现未覆盖字段;类型推断本身不等于运行时验证。
九、FAQ
问:推断出的 optional 一定正确吗?答:它只表示当前样本中缺失,未来数据仍可能始终提供。
问:对象数组应该生成 union 还是合并对象?答:若元素代表同一实体,通常合并字段并标 optional;不同实体则应保留 union 或拆分模型。
问:为什么不自动生成所有业务类型?答:日期、ID、金额和枚举需要契约,JSON 样本无法可靠证明。
十、总结
JSON→TypeScript 推断应递归遍历对象和数组,合并数组元素的共同字段,标记样本中缺失的 optional,并对冲突生成 union。命名、null、空数组和业务语义都需要保守提示;最终类型仍需编译和人工确认。
来源与延伸阅读
技术审校所依据的规范与权威参考资料。
相关文章
JSON 转 TypeScript:null、字段缺失、optional 与 union
区分 JSON 中字段缺失与 null,说明 TypeScript optional、strictNullChecks 和 union 类型在接口建模中的不同含义。
最佳实践OpenAPI/Swagger Schema 到 TypeScript 契约:代码生成与运行时校验
使用 OpenAPI 术语说明 schema、required、nullable、请求/响应契约到 TypeScript 的生成边界,并区分静态代码生成与运行时校验。
错误排查JSON 转 SQL 类型推断:MySQL 与 PostgreSQL 的边界
分开说明 MySQL 与 PostgreSQL 对 JSON、数字、字符串、数组、对象和 NULL 的类型建议,解释为什么样本推断不能替代明确的数据库 schema。
继续阅读
可打开关联的浏览器工具,使用自己的样本验证文中的处理流程。
打开关联工具