Javascript is required
实现原理发布于 2026-07-28审校于 2026-08-086 分钟阅读

JSON 转 TypeScript 类型推断引擎:递归、数组合并与命名

JSON 转 TypeScript 不是把当前值的 typeof 逐个打印出来,而是递归处理对象、数组和混合样本。数组需要合并元素类型,字段名需要生成合法且稳定的类型键;单个样本仍无法证明字段必有、范围完整或未来不会出现其他形状。

JSON to TypeScriptType InferenceRecursive TypesArrays

一、问题概述:样本值只是类型证据的一部分

[{"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、空数组和业务语义都需要保守提示;最终类型仍需编译和人工确认。

来源与延伸阅读

技术审校所依据的规范与权威参考资料。

相关文章

继续阅读

可打开关联的浏览器工具,使用自己的样本验证文中的处理流程。

打开关联工具