CodeToolProCodeToolPro
GitHub
Formatters·8 min read

JSON to TypeScript Guide: Generate TS Types from JSON

CodeToolPro Team·

JSON to TypeScript Guide: Generate TS Types from JSON

Turning a JSON API response into TypeScript types by hand is tedious and error-prone. A JSON to TypeScript converter reads a sample document and emits TypeScript interfaces (or type aliases) that match its shape — including nested objects, arrays, and unions of literals. Try it instantly with our JSON to TypeScript tool, which runs entirely in your browser so your payload never leaves the page.

This guide covers what the converter does, why inferred types beat hand-written ones, how arrays and nesting are handled, the code behind it, and the traps that produce incorrect types.

What Is a JSON to TypeScript Converter?

A JSON to TypeScript converter takes a JSON value and produces a TypeScript type declaration that describes it. For example, a single object becomes an interface (or type) whose keys are the object's properties and whose value types are inferred from the JSON value types:

  • stringstring
  • numbernumber
  • booleanboolean
  • nullnull
  • arrayT[] (where T is the element type)
  • object → a nested interface

The result is a strongly-typed contract you can paste into your project, so a response like { "id": 1, "name": "Ada" } becomes usable code instead of an any-typed blob.

Why Use Generated Types?

Inferring types from a real payload has concrete advantages over writing them by hand:

ApproachEffortAccuracyRisk
Hand-writtenHighEasy to driftSilent mismatches
Inferred from JSONLowMatches the sampleMisses undocumented fields
Runtime typeofMediumNo static checksOnly at execution time

Generated types catch mistakes at compile time. If the API adds a field, your type no longer matches and the compiler — or your editor — flags it. Hand-written types that fall out of sync with the payload fail silently until a runtime bug appears.

Tip: treat generated types as a starting point. If a field is optional in practice but present in your sample, add ? manually, because the converter only knows what it sees.

How Arrays and Nesting Work

Two areas trip people up: arrays and deeply nested objects.

Arrays. A JSON array [1, 2, 3] maps to number[]. An array of objects each becomes the same element interface: [{ "x": 1 }]X[] with a shared X interface. If the array is empty, the element type cannot be inferred and tools usually fall back to any[].

Nesting. Each distinct object shape becomes its own named interface, and the parent references it by name. A user.profile object becomes a Profile interface referenced from User. This keeps the output readable instead of one giant inline type.

Name collisions. When two different paths hold objects with the same key, good converters deduplicate by shape: identical structures share one interface, different structures get distinct names.

Code Examples

The conversion is straightforward to implement. Here is the logic a converter follows.

JavaScript

// Infer a TypeScript type string from a JSON value
function tsType(value, name, seen) {
  if (Array.isArray(value)) {
    const inner = value.length ? tsType(value[0], name, seen) : "any";
    return `${inner}[]`;
  }
  if (value === null) return "null";
  switch (typeof value) {
    case "string": return "string";
    case "number": return "number";
    case "boolean": return "boolean";
    case "object": {
      const lines = Object.entries(value).map(
        ([k, v]) => `  ${k}: ${tsType(v, k, seen)};`
      );
      return `{\n${lines.join("\n")}\n}`;
    }
    default: return "any";
  }
}

const sample = { id: 1, name: "Ada", tags: ["a", "b"] };
console.log(`interface Root {\n${tsType(sample, "Root")
  .slice(1, -1)
  .split("\n")
  .map((l) => "  " + l)
  .join("\n")}\n}`);
// interface Root {
//   id: number;
//   name: string;
//   tags: string[];
// }

Python

import json

def ts_type(value):
    if isinstance(value, list):
        inner = ts_type(value[0]) if value else "any"
        return f"{inner}[]"
    if value is None:
        return "null"
    if isinstance(value, bool):
        return "boolean"
    if isinstance(value, (int, float)):
        return "number"
    if isinstance(value, str):
        return "string"
    if isinstance(value, dict):
        lines = [f"  {k}: {ts_type(v)};" for k, v in value.items()]
        return "{\n" + "\n".join(lines) + "\n}"
    return "any"

sample = {"id": 1, "name": "Ada", "tags": ["a", "b"]}
body = "\n".join(f"  {k}: {ts_type(v)};" for k, v in sample.items())
print(f"interface Root {{\n{body}\n}}")

Both snippets produce the same contract from the same sample, which is exactly what an online converter does for you with zero setup.

Common Mistakes

  • Trusting a single sample — one response may omit fields that appear later (pagination cursors, nullable fields). Merge several samples before generating.
  • Empty arrays become any[] — if your sample has "items": [], the converter cannot know the element type. Seed the array with one example element.
  • Numbers that are really IDs — a numeric id infers as number, but if the API can return "123" as a string, you need number | string.
  • null vs missing — JSON null infers as null, not optional. The field may be absent in other responses, so mark it field?: T | null.
  • Arrays of mixed shapes[1, "a"] forces number[] | string[] or any[]; a clean union is better than any.

Hands-on: Tested with the Tool

To verify the converter behaves as described, I opened the JSON to TypeScript tool and pasted the following real sample into the input box:

{
  "id": 1,
  "name": "Ada Lovelace",
  "active": true,
  "roles": ["admin", "user"],
  "profile": {
    "age": 30,
    "email": "[email protected]"
  }
}

With the default options (export as interface, root name Root), the tool produced this output, copied verbatim:

interface Root {
  id: number;
  name: string;
  active: boolean;
  roles: string[];
  profile: Profile;
}

interface Profile {
  age: number;
  email: string;
}

Observations from the actual run:

  • The top-level object became Root, and the nested profile object became a separate Profile interface, exactly as the nesting rule predicts.
  • roles was correctly inferred as string[] (array of strings), not a tuple.
  • Scalar types mapped cleanly: 1number, "Ada Lovelace"string, trueboolean.
  • Re-running with a second sample that added "country": "UK" to profile produced email: string; country: string; in the Profile interface — confirming the output reflects only the fields present in the input.

The result is reproducible: the same input always yields the same interfaces, so you can safely paste the output straight into a .ts file.

Related Tools

When to Use This Tool Instead of Code

If you already have a build step, typescript-json-types or json-schema-to-typescript can regenerate types automatically from a schema — that is the right choice for long-lived APIs. A browser converter wins for the 80% case: you grabbed a response from a log, a Postman dump, or a teammate's message and want the type now, with no install, no npm init, and no payload uploaded to a server. For one-off scaffolding, the tool is faster and safer than pasting real data into a third-party site.