Javascript is required
How it worksPublished 2026-07-286 min read

JSON to TypeScript: Type Inference and Array Merging

JSON-to-TypeScript is more than printing typeof for each current value. Objects and arrays require recursion, array elements need merging, and property names need stable TypeScript keys. A single sample cannot prove requiredness, ranges, or future shapes.

By: Tiny
JSON to TypeScriptType InferenceRecursive TypesArrays

1. Problem: a sample is only partial type evidence

An array such as [{"id":1},{"id":2,"name":"Ada"}] must merge element objects and mark name optional. Copying the first element loses fields; emitting a raw union can make shared properties awkward for consumers.

2. Minimal reproduction: recurse into objects and arrays

This inference function accepts JSON values only. It labels an empty array unknown[] instead of guessing an element type.

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)[] }

3. Root cause: array merging needs common fields and optionality

Object-array elements can have different key sets. The engine must collect keys, merge same-key value types, and mark missing keys optional. Conflicting scalars should become a union rather than silently choosing the first type. Recursion depth and cycles also need limits.

4. Recommendation: emit conservative types and review names

Keep property names when possible. Keys that are not identifiers can use quoted property names or a documented rename map. Warn about empty arrays, null, mixed arrays, and deep objects; inferred output is not a complete schema.

5. Complete code: merge object arrays into a type string

This small engine handles recursive objects, object-array merging, optional fields, and basic unions. It is intentionally not a naming-conflict resolver, cycle detector, or runtime validator.

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 })[]

6. Incorrect approaches

Classifying every number as integer or float does not infer business ranges. Taking the first array element loses later fields. Concatenating an invalid key into an interface creates uncompilable output. A type string without sample limitations also looks more authoritative than it is.

7. Boundaries: null, mixed arrays, and names

Arrays can mix null, scalars, and objects, so unions should be conservative. An all-null field has no non-null type evidence. Preserve original names or use a documented rename map, and limit recursion and sample size.

8. Verification

Test object arrays with missing keys, null, empty arrays, mixed scalars, and keys containing spaces or hyphens. Feed generated text to the TypeScript compiler and try additional samples. Inference is not runtime validation.

9. FAQ

Is inferred optional always correct? It means only that the key was missing in the sample; future data may always provide it.

Should object arrays become a union or merged object? Merge fields when elements are one entity shape; preserve a union or split models for different entities.

Why not infer every business type? Dates, IDs, money, and enums need a contract that JSON samples cannot prove.

10. Summary

A JSON-to-TypeScript engine should recurse through objects and arrays, merge common array fields, mark sample-missing keys optional, and use unions for conflicts. Names, null, empty arrays, and business meaning need conservative warnings, followed by compilation and review.

Sources and further reading

Specifications and references for checking the article’s technical details.

Related articles

Keep exploring

Use the related browser tool to test the workflow with your own sample data.

Open related tool