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: stringdoesn’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. Addas const-style narrowing or a validator. - String-literal types. A
statusfield that’s always one of three values is best typed as"draft" | "published" | "archived". Generators emitstringunless you explicitly hint.
Common pitfalls
- Treating
as Typeas validation. TypeScript casts are compile-time only;JSON.parse(text) as Userdoesn’t actually check the runtime value matchesUser. Use a validator (Zod, io-ts) for safety. - Forgetting
strictNullChecks. Without it,stringandstring | nullare 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; considerreadonlyif 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.