Skip to content

JSON to TypeScript

Generate TypeScript interfaces from a JSON sample.

This tool generates TypeScript interface definitions from a sample JSON value in your browser; nothing leaves your machine. The output captures the shape of the JSON; production code typically refines the types after.

The basic mapping

JSON:
{
  "name": "Alice",
  "age": 30,
  "active": true
}

Generated TypeScript:
interface Root {
  name: string;
  age: number;
  active: boolean;
}

TypeScript’s primitive types map cleanly to JSON’s: string, number, boolean. JSON’s null is null (or undefined if the field is optional).

Interface vs type alias

Two roughly-equivalent forms in TypeScript:

interface User {
  name: string;
  age: number;
}

type User = {
  name: string;
  age: number;
};

Practical differences:

  • Interfaces can be re-opened (declared multiple times to extend); type aliases can’t.
  • Type aliases can express union types (type Status = "draft" | "published"); interfaces can’t.
  • Interfaces generate slightly better error messages in some cases.

For object-shape definitions (which is most JSON-derived code), either works. The tool above defaults to interface; switch to type if you prefer or if the project’s style demands it.

Optional fields

JSON’s null and absent keys both suggest optionality. TypeScript distinguishes:

interface User {
  name: string;          // required
  email?: string;        // optional — may be undefined
  nickname: string | null; // present, but value can be null
}

The trap: email?: string says the property can be missing or undefined. email: string | undefined says it must be present but can be undefined. They’re different — the first allows User = { name: "A" }; the second requires User = { name: "A", email: undefined }.

In strict null-checking mode ("strictNullChecks": true in tsconfig), the distinction matters. For JSON parsing, the ? form is usually correct.

Nested objects

JSON:
{
  "user": {
    "name": "Alice",
    "address": {
      "city": "NYC"
    }
  }
}

Generated TypeScript:
interface Address {
  city: string;
}

interface User {
  name: string;
  address: Address;
}

interface Root {
  user: User;
}

Nested types are extracted into their own definitions. Some generators inline anonymous types instead:

interface Root {
  user: {
    name: string;
    address: {
      city: string;
    };
  };
}

Inline is compact for one-off use; named types are easier to share across files and to mock.

Arrays

JSON: [1, 2, 3]
TypeScript: number[]

JSON: [{"name": "Alice"}]
TypeScript: { name: string }[]   or   Item[]

For arrays of objects, the element type is inferred from the first element. For mixed-type arrays, the type is a union: (number | string)[].

Tuple types

A subtle case. JSON arrays where the element types differ by position:

[1, "two", true]

TypeScript can express this as a tuple:

type Mix = [number, string, boolean];

But most JSON-from-the-wild uses arrays as homogeneous collections, not tuples. The tool above types an array from its first element only — [1, "two", true] becomes number[] — so widen mixed arrays to a union or a tuple by hand.

Union types from multiple samples

With several samples, a generator can produce a union type (the tool above takes one sample, so write this by hand):

Sample 1: {"type": "circle", "radius": 5}
Sample 2: {"type": "square", "side": 3}

Generated:
type Shape =
  | { type: "circle"; radius: number }
  | { type: "square"; side: number };

The type field is a string-literal union ("circle" | "square"), which makes TypeScript’s narrowing work:

function area(s: Shape) {
  if (s.type === "circle") return Math.PI * s.radius ** 2;
  return s.side ** 2;
}

This is discriminated unions — TypeScript’s idiomatic way to model JSON polymorphism. From a single sample no generator can infer the discriminator.

Number precision

TypeScript has only number (a 64-bit IEEE 754 double). Big integers don’t fit. JSON 12345678901234567890 parses as a number and silently loses precision (becomes 12345678901234567000).

For known big-integer fields, modern TypeScript supports bigint:

interface User {
  id: bigint;
}

But JSON can’t natively represent bigint. The receiving code has to convert from string:

const data = JSON.parse(text) as { id: string };
const user = { ...data, id: BigInt(data.id) };

Most APIs that need exact-integer precision encode the value as a string for this reason.

What types can’t capture

  • Validation rules. email: string doesn’t enforce the @ requirement. Use Zod, io-ts, or another runtime validator.
  • Required vs optional inferred from one sample. Single-sample input may be wrong about what’s optional. Verify against schema or multiple samples.
  • Numeric ranges. A field that’s always 0–100 is just number. Add as const-style narrowing or a validator.
  • String-literal types. A status field that’s always one of three values is best typed as "draft" | "published" | "archived". Generators emit string unless you explicitly hint.

Common pitfalls

  • Treating as Type as validation. TypeScript casts are compile-time only; JSON.parse(text) as User doesn’t actually check the runtime value matches User. Use a validator (Zod, io-ts) for safety.
  • Forgetting strictNullChecks. Without it, string and string | null are not distinguished. Always enable it for new TypeScript projects.
  • Single-sample optional inference. Capture variations to identify truly-optional fields.
  • Mutable arrays where readonly is intended. number[] is mutable; readonly number[] is read-only. JSON-parsed data is conceptually read-only; consider readonly if your code shouldn’t mutate the input.
  • Index signatures for objects with arbitrary keys. A JSON object whose keys are dynamic (e.g., user IDs as keys) is { [key: string]: ValueType }, not a fixed-key interface. Generators sometimes get this wrong.

For other JSON-to-language conversions, see JSON to Go struct, JSON to Python dataclass.