How to Convert JSON to TypeScript

A JSON sample tells you the shape of your data, but TypeScript wants that shape written down as an interface or type. Converting between the two is mostly mechanical — map each JSON value to its TypeScript equivalent — but a few decisions (optional vs nullable, arrays of objects, what to do with a lone null) are where correctness actually lives. This guide walks the type mapping, the worked output, the tools that automate it, and the pitfalls a single sample hides. Generate types instantly from any JSON with the JSON to TypeScript converter.

The Core Idea

Converting JSON to TypeScript is a structural mapping: every JSON value has a TypeScript counterpart, and you apply the mapping recursively until the whole document is described.

  • stringstring
  • numbernumber (TypeScript has one numeric type; JSON doesn't distinguish int from float either)
  • booleanboolean
  • object → a named interface describing its keys
  • arrayT[], where T is the type of the elements
  • nullnull — but this is the ambiguous case, covered below

The mechanical part is easy enough that tools do it instantly. What a tool can't infer from a single sample is which fields are optional, which are nullable, and what the real range of values is. That judgment is where most of this guide lives.

Worked Example

Given this JSON — an object with a nested object and an array of objects:

{
  "id": 42,
  "name": "Ada Lovelace",
  "active": true,
  "profile": {
    "bio": "Mathematician",
    "website": null
  },
  "posts": [
    { "slug": "first-post", "views": 120 },
    { "slug": "second-post", "views": 9 }
  ]
}

The generated TypeScript:

interface User {
  id: number;
  name: string;
  active: boolean;
  profile: Profile;
  posts: Post[];
}

interface Profile {
  bio: string;
  website: null;
}

interface Post {
  slug: string;
  views: number;
}

Each nested object becomes its own named interface (Profile, Post) rather than an inline type — this keeps the output readable and reusable. The posts array of two same-shaped objects collapses to Post[]. Note website: null: the sample only ever shows null, so the converter has nothing else to go on. You almost certainly want website: string | null there — fix it by hand or feed a second sample with a real URL.

interface vs type Alias

Both can describe an object shape, and for plain JSON records they're nearly interchangeable:

interface User { id: number; name: string; }
// vs
type User = { id: number; name: string; };

The common convention — and what most converters emit — is interface for object shapes. Interfaces can be extended (interface Admin extends User), can be re-opened and merged across declarations, and tend to render more cleanly in editor tooltips and error messages.

Reach for a type alias when you need something an interface can't express:

type Status = "open" | "closed" | "pending";  // union of literals
type ID = string | number;                     // alias a union of primitives
type Point = [number, number];                 // tuple
type WithTimestamp<T> = T & { createdAt: string };  // intersection

Rule of thumb: interface by default for the object shapes your JSON produces; type when the shape is a union, intersection, tuple, or mapped type.

Nulls and Optionals — the Hard Part

This is the single biggest source of wrong generated types. TypeScript has three distinct ways to say "this field might not have a real value," and JSON can't tell you which one you want from one sample.

interface Account {
  email: string;            // always present, always a string
  phone?: string;          // may be absent from the object entirely
  middleName: string | null; // always present, but the value can be null
  nickname?: string | null;  // may be absent, and may be null when present
}
  • field?: T — the key may be missing from the object. Accessing it yields undefined.
  • field: T | null — the key is always present, but its value may be null.
  • field: T | undefined — the key is present and its value may be the literal undefined (rare in JSON, since JSON has no undefined — you'll see ? far more often).

The trap: when your one sample shows "website": null, a converter writes website: null. That's almost never what you mean. The field is probably a nullable string (string | null) or an optional one (website?: string). And when the sample simply omits a field, the converter can't mark it optional — it just won't appear in the interface, so the next response that includes it won't be typed at all.

The only reliable fix is more data. Feed the converter multiple real responses — some with the field present, some without, some with a populated value, some null — and it can infer field?: string | null correctly. quicktype does exactly this when you pass it several samples (see below). With one sample, treat every null in the output as a flag to review by hand.

Arrays

Arrays are where the element type matters and where empty or mixed arrays bite.

// Homogeneous primitives → T[]
[1, 2, 3]              →  number[]
["a", "b"]             →  string[]

// Array of same-shaped objects → named interface + T[]
[{ "x": 1 }, { "x": 2 }]   →  interface Item { x: number } ... Item[]

// Empty array → no element type to infer
[]                     →  any[]  (or unknown[], or never[] depending on tool)

// Mixed types → a union, or any[]
[1, "two", true]       →  (number | string | boolean)[]

// Fixed-length, position-typed → a tuple (you must opt in)
[12.9, 77.5]           →  [number, number]   // e.g. a lat/lng pair

Two things to watch. First, an empty array tells the converter nothing about its element type, so you get any[] — feed a sample where the array has items, or annotate it yourself. Second, converters default to T[], not tuples; if a JSON array is really a fixed-length, position-typed pair (coordinates, RGB triples), you'll want a tuple type like [number, number], which you have to write or configure deliberately — inference treats it as number[].

Tools

You rarely write these interfaces by hand. The main options:

  • quicktype — the most popular. Available as a CLI, at quicktype.io, and as a library. It infers types from one or many samples (merging them to detect optionals and unions) and can target dozens of languages, not just TypeScript.
  • json-to-ts / json2ts — lighter, TypeScript-only. Good for a fast paste-and-go interface without quicktype's configuration surface.
  • transform.tools — a browser playground with JSON → TypeScript among many other conversions.
  • Janeer JSON to TypeScript — does the conversion entirely in your browser, so the payload never leaves your machine. Useful for sensitive or internal API responses.

A typical quicktype CLI run, merging two sample responses so optional and nullable fields are detected:

# npm install -g quicktype

# Single sample → User interface in types.ts
quicktype user.json -o types.ts --lang typescript --top-level User

# Merge multiple real responses so optionals/nullables are inferred
quicktype response-1.json response-2.json response-3.json \
  -o types.ts --lang typescript --top-level User

# Pipe straight from an API while developing
curl -s https://api.example.com/users/42 | quicktype -l typescript --top-level User

Passing several real responses is the difference between types that match production and types that were right for exactly one row of data.

Why One Sample Isn't Enough

It's worth stating plainly because it's the mistake everyone makes first: a single JSON example cannot describe a varying data source. From one response you can't know:

  • Which fields are optional — they're simply present in this response and absent in others.
  • Which fields are nullable — they happen to be populated (or happen to be null) in this one sample.
  • What the full set of enum-like values is — a status field that's "active" here might be one of five strings in practice, which you'd want as "active" | "inactive" | ... rather than string.
  • Whether an array that looks homogeneous is actually mixed in other responses.

Types generated from one sample tend to be too strict — required fields that should be optional — and the gap surfaces at runtime, not compile time. Validate the generated types against a representative batch of real responses (or merge that batch with quicktype), and treat the output as a starting point you review, not a finished contract. If you control the API, generating types from its JSON Schema or OpenAPI spec is more reliable than inferring from samples at all.

Edge Cases and Pitfalls

Number precision

JSON numbers and TypeScript number are both IEEE-754 doubles, so very large integers lose precision (anything beyond Number.MAX_SAFE_INTEGER, 2^53 − 1). If your JSON carries 64-bit IDs or timestamps as numbers, they may already be corrupted by JSON.parse. The fix is to transport such values as strings and type them as string (or use bigint with a custom reviver). Inference can't catch this — it just sees number.

Dates stay strings

JSON has no date type, so an ISO timestamp like "2026-06-17T10:00:00Z" is just a string and converters type it as string. There's no Date in the generated output. Convert to Date yourself after parsing, or brand the field (type ISODate = string) to document the intent.

Invalid identifiers and reserved words as keys

JSON keys can be anything — "first-name", "2fa", "class", even "". These aren't valid bare TypeScript property names, so converters quote them: "first-name": string;. Quoted keys work, but you'll access them with bracket notation (obj["first-name"]). For objects with arbitrary or open-ended keys (a map keyed by user ID), an index signature or Record is the right call — see below.

Dictionaries: Record vs a fixed interface

If a JSON object is really a map — keys are data (user IDs, locale codes), not a fixed schema — don't generate one interface property per key. Use a dictionary type instead:

// JSON: { "en": "Hello", "fr": "Bonjour", "de": "Hallo" }

// Wrong: an interface that breaks the moment a new locale appears
interface Greetings { en: string; fr: string; de: string; }

// Right: an open-ended dictionary
type Greetings = Record<string, string>;
// or:  interface Greetings { [locale: string]: string; }

A converter can't always tell a record from a struct, so this is a manual judgment: fixed, known keys → interface; open-ended keys → Record or an index signature.

Recursive and unknown structures

Self-referential JSON (a tree of comments with nested replies) needs a recursive interface — interface Comment { replies: Comment[] }. quicktype handles this; simpler tools may not. And for genuinely free-form blobs, prefer unknown over any so the compiler forces you to narrow before use. Additional or unexpected properties that aren't in your sample simply won't be typed — if you need to allow them, add an index signature.

Try It Live

The JSON to TypeScript converter turns any JSON sample into interfaces instantly, right in your browser — paste an API response and copy the types out. Clean up the input first with the JSON formatter so malformed JSON doesn't trip the conversion, and when you want a validation contract instead of types, derive one with the JSON Schema generator from the same sample.

Frequently Asked Questions

How do I convert a JSON object to a TypeScript interface?

Map each JSON value to its TypeScript equivalent: a string becomes string, a number becomes number, a boolean becomes boolean, a nested object becomes its own named interface, and an array becomes T[] where T is the type of its elements. Wrap the whole thing in an interface declaration named after the data. Tools like quicktype, json-to-ts, and the in-browser converter automate this — paste the JSON and they emit the interface. The only judgment a tool can't make from one sample is which fields are optional or nullable.

Should I use an interface or a type alias for JSON shapes?

Use an interface for object shapes — it is the common convention, it can be extended and merged, and editor tooltips render it cleanly. Use a type alias when you need something an interface cannot express: unions (type Status = "open" | "closed"), intersections, mapped types, or aliasing a primitive or tuple. For a plain JSON record, interface User { id: number } and type User = { id: number } are nearly interchangeable, so reach for interface by default and switch to type only when the shape demands it.

How should I handle null values when converting JSON to TypeScript?

This is the central correctness issue, because a single sample is ambiguous. If a field holds null in your sample, you cannot tell whether the field is always present but nullable (field: string | null), sometimes absent (field?: string), or both (field?: string | null). A bare null value gives you no element type at all, so a tool will fall back to null or any. The fix is to feed multiple real samples — some with the field populated, some without — so the tool can infer the union and optionality correctly rather than guessing from one row.

What is the best tool to convert JSON to TypeScript?

quicktype is the most popular — available as a CLI (quicktype data.json -o types.ts), at quicktype.io in the browser, and as a library. It infers types from one or many samples, merges multiple inputs to detect optionals, and targets many languages beyond TypeScript. Lighter options include json-to-ts and the json2ts service, plus transform.tools for quick paste-and-go. The Janeer JSON to TypeScript converter does it entirely in your browser, so sensitive payloads never leave your machine.

Why is one JSON sample not enough to generate accurate types?

One sample shows one possible shape, but real data varies. A single example cannot reveal which fields are optional (absent in other responses), which are nullable, what the full set of enum-like string values is, or whether an array that looks homogeneous sometimes contains other types. Generating types from one response often produces something too strict — it marks optional fields as required — and you only discover the gap at runtime. Feed several real API responses (quicktype merges them), or validate the generated types against a representative batch before trusting them.