How to generate typescript react prop types from json
- Step 1Capture the most complete props sample — Grab the JSON the component actually receives — a Network-tab response, a
console.log(JSON.stringify(props, null, 2))from a parent, or a Storybook story'sargs. If the data is an array, pick the row with every optional field populated, because the generator samples the first array element only. - Step 2Paste it and name the root — Drop the JSON file or paste it in. Set 'Root type name' to your component's prop type, e.g.
UserCardProps. Nested objects keep auto-names derived from their keys (addressbecomesAddress). - Step 3Choose interface or type — Leave 'Use interface' on for the idiomatic React convention (
interface UserCardProps). Switch it off if you plan to intersect the shape withReact.HTMLAttributes<...>later, where atypealias reads more naturally. - Step 4Decide on the optional switch — Leave 'Optional properties' off for a strict starting shape. Turn it on only if you want a fully-permissive draft; remember it marks every field
?, so you'll usually generate strict and then add?to the genuinely-optional props by hand. - Step 5Generate and copy — Click 'Generate Types'. Review the type count, then Copy or Download .ts. The download is named
<input>.types.ts. - Step 6Wire it into the component — Rename the root if needed and apply it:
const UserCard = ({ name, email, address }: UserCardProps) => { ... }. Replace anyfield: nullplaceholders with the realstring | nullwidths, and add string-literal unions for enum-like props such asvariant: 'primary' | 'secondary'.
The four real generator options
These are the only controls in the tool's UI (app/tool/json-to-typescript). There is no indent toggle (output is always 2-space), no per-field optional detection, and no Date / branded-type inference — the generator maps JSON types to TS types one-to-one.
| UI control | What it does | Default | Notes |
|---|---|---|---|
| Root type name (text box) | Names the outermost type. Root by default; type User, ApiResponse, etc. | Root | Nested object types are named from their key (a address key becomes interface Address), not from this box |
Use interface (vs type) | Emits interface Name { ... } when on; type Name = { ... }; when off | On | Both forms are structurally identical; the toggle is purely stylistic |
| Export all types | Prefixes every emitted type with export | On | Turn off for types you'll re-export from a barrel file or keep module-private |
| Optional properties (?) | Adds ? to every field of every type at once | Off | This is a single global switch — it does NOT detect which keys are sometimes-missing. See the edge cases |
How JSON values map to TypeScript
The mapping is driven entirely by the JavaScript runtime type of each parsed value — there is no schema, no format detection, and no heuristics beyond these rules.
| JSON value | Generated TS type | Detail |
|---|---|---|
"text" | string | Every string is string — no string-literal union, no email/uuid/date-string narrowing |
42 or 9.5 | number | Integers and floats both become number. (The sibling json-to-zod does emit .int() for integers — TypeScript has no integer type, so this tool can't) |
true / false | boolean | No literal true/false narrowing |
null | null | A null value produces the literal type null — not string | null. There is no nullable inference |
{ ... } | named interface/type | Named from the key (or the root name). Emitted as a separate top-level type and referenced by name |
[ ] (empty) | unknown[] | An empty array can't be sampled, so the element type is unknown |
[1, 2, 3] | number[] | Element type inferred from the array contents |
[1, "a"] | (number | string)[] | Mixed-type arrays produce a union element type |
[{...}, {...}] | <Name>Item[] | Object arrays produce one item interface — sampled from the first element only |
Free vs Pro generation limits
JSON to TypeScript is a Pro tool gated by the shared codeGeneration quota (lib/preview-quotas.ts). Limits are daily-run counts plus a row cap that applies when the JSON root is an array.
| Tier | Runs/day | Array row cap | File size |
|---|---|---|---|
| Free | 1 | 100 rows | 2 MB |
| Pro | 5 | 1,000 rows | 100 MB |
| Pro Media | 50 | 10,000 rows | 100 MB |
| Developer / Enterprise | Unlimited | Unlimited | 100 MB+ |
Cookbook
Real props payloads in, the exact TypeScript the generator emits out, and the hand-edits a React developer makes afterward.
A flat card-props object to an interface
ExampleThe everyday case: a UserCard receives a flat object. Set the root name to UserCardProps, leave defaults, generate.
Input (props payload):
{ "name": "Ada Lovelace", "email": "ada@x.com", "verified": true, "followers": 1240 }
Output (rootName: UserCardProps):
export interface UserCardProps {
name: string;
email: string;
verified: boolean;
followers: number;
}
Usage:
const UserCard = ({ name, email, verified, followers }: UserCardProps) => { /* ... */ };Nested object becomes a reusable named interface
ExampleA prop with a nested address object yields a separate Address interface you can import into child components — no inline object literal type.
Input:
{ "name": "Ada", "address": { "city": "London", "postcode": "EC1" } }
Output:
export interface Root {
name: string;
address: Address;
}
export interface Address {
city: string;
postcode: string;
}
// Rename Root to ProfileCardProps; reuse Address in <AddressLine props />.List props: array sampled from the first row
ExampleA rows prop holding an array of objects produces one RowsItem interface — sampled from the first element. If later rows have extra keys, they won't appear; reorder so the richest row is first.
Input:
{ "rows": [ { "id": 1, "label": "A", "badge": "new" }, { "id": 2, "label": "B" } ] }
Output:
export interface Root {
rows: RowsItem[];
}
export interface RowsItem {
id: number;
label: string;
badge: string;
}
// badge came from the first row; row 2's missing badge is NOT flagged optional.Optional switch for a permissive draft
ExampleFor a component still in flux, flip 'Optional properties' on to get every prop as ?, then delete the ? from the props you've settled on.
Input:
{ "title": "Hello", "subtitle": "World", "dismissible": true }
Output (Optional properties: ON):
export interface Root {
title?: string;
subtitle?: string;
dismissible?: boolean;
}
// Every field is optional. Make title/subtitle required by hand once finalized.Null and enum props need a hand-edit
ExampleA null sample value and an enum-like string both come out under-typed; the generator gives you the scaffold, you widen it.
Input:
{ "avatarUrl": null, "status": "active" }
Output (verbatim):
export interface Root {
avatarUrl: null;
status: string;
}
Hand-edit for a real prop type:
export interface BadgeProps {
avatarUrl: string | null;
status: "active" | "inactive" | "pending";
}Errors and edge cases
Real errors and silent failures sourced from each platform's own documentation. Match the wording to the row, fix what the row says to fix.
Array prop sampled from first element only
By designWhen a prop is an array of objects, the item interface comes from the first element. Keys that appear only in later elements are missing from the type. Reorder your sample so the most complete object is first, or merge a superset object by hand before pasting.
'Optional' switch marks every prop, not the missing ones
By designThe optional checkbox adds ? to all fields globally; it never analyzes which props are sometimes-absent. Generate strict (box off) and add ? to the genuinely-optional props, or use json-to-zod for runtime presence checks per field.
Enum-like string prop typed as `string`
ExpectedA variant: "primary" value becomes variant: string — there's no string-literal union inference. Replace with variant: "primary" | "secondary" by hand; the generator can't know the full set of allowed values from one sample.
Nullable prop typed as `null`
ExpectedavatarUrl: null in the sample yields avatarUrl: null. Widen to string | null (or the real type) manually. Capturing a sample where the field is non-null instead gives you the underlying type directly.
Two nested props share a key name
First write winsIf header and footer both contain a meta object of different shapes, only the first Meta interface is emitted and both references point to it. Rename one JSON key before generating, or generate the branches separately.
Empty array prop
unknown[]An empty tags: [] can't be sampled, so it becomes tags: unknown[]. Replace with the intended element type, e.g. string[], by hand. Provide at least one element in the sample to have it inferred.
ReactNode / children prop not representable in JSON
Not inferablechildren, render-prop callbacks, and event handlers like onClick aren't JSON values, so they can't appear in a generated type. Add them by hand: children: React.ReactNode; onClick: () => void; after pasting the data-only interface.
Date arrives as a string
ExpectedJSON has no Date type, so an ISO timestamp like createdAt: "2026-06-10T..." becomes createdAt: string. That's correct for the serialized prop; convert to a Date inside the component if needed.
Invalid JSON pasted
Parse errorThe generator calls JSON.parse on a trimmed copy of the input; malformed JSON (trailing commas, single quotes, unquoted keys) throws and the error surfaces in the UI. Fix the source first with json-format-fixer, then regenerate.
Frequently asked questions
Can I use the generated interface directly as React.FC props?
Yes. Rename the root to <Component>Props and apply it either as const C = ({...}: ComponentProps) => {} (the modern preference) or React.FC<ComponentProps>. The generated shape is a plain object interface, exactly what a props type needs — you'll typically still hand-add non-data props like children, className, and event handlers, which can't appear in JSON.
The 'Optional properties' checkbox made every field optional — how do I make only some optional?
That checkbox is a single global switch: when on, it adds ? to every field of every generated type at once. It does not analyze which keys are sometimes-present. There is no per-field optional detection in this tool — even when the root is an array whose elements have different keys, the generator samples only the first element, so genuinely-optional keys are not discovered. Generate with the box off to get a strict shape, then hand-add ? to the specific fields you know are optional. If you need real runtime presence checks, generate a schema with json-to-zod and chain .optional() per field.
My JSON is an array of objects with varying keys — why are some fields missing from the type?
When the root (or any nested value) is an array of objects, the generator infers the item interface from the first element only. Keys that appear in later elements but not the first are absent from the generated type; keys in the first element but not later ones are present and non-optional. Reorder your sample so the most complete object is first, or merge a representative superset object by hand before pasting. This is the single most common surprise — the tool samples, it does not union across array elements.
How do I get a string-literal union for a `variant` prop?
You add it by hand. Every JSON string maps to string — the generator has no way to know the full set of allowed values from a single sample, so a variant: "primary" value becomes variant: string. Replace it with variant: "primary" | "secondary" | "ghost" after generating. This is the right division of labor: the tool captures the data shape, you encode the domain constraints.
A field came out as `: null` — that's not a useful type. Why?
JSON null maps to the literal TypeScript type null, because that's the only information present in the sample — the generator has no schema telling it the field is string | null. Wherever you see field: null, the real intent is almost always field: string | null (or another type) and you should widen it by hand. Capture a sample where the field has a non-null value if you want the underlying type inferred instead.
Two different nested objects share a key name — why did one shape get lost?
Nested object types are named from their key, and the generator registers each name only once (first write wins). If a meta object appears in two places with different shapes, both references point to the single Meta interface built from whichever was walked first; the second shape is silently discarded. Rename one of the JSON keys before generating, or generate the two branches separately and rename the resulting interfaces. The collision is structural, not a bug you can toggle off.
Will `children` or `onClick` show up in the generated props?
No — those aren't JSON-serializable, so they can't appear in a sample and the generator never emits them. After pasting your data-only props, add the non-data members by hand: children: React.ReactNode; onClick: (id: string) => void;. Think of the generated interface as the data half of your props, which you extend with the behavioral half.
Can I stop the tool from exporting every type?
Yes — uncheck 'Export all types' and the export prefix is dropped from every emitted type. That's useful when you want a single barrel file to control your public surface, or when the generated types are module-private helpers. The toggle is all-or-nothing; there's no per-type export control, so for mixed visibility, generate with export off and add export to the specific types you want public.
How do I get the output into my project?
Use the Copy button to put the generated source on your clipboard, or Download .ts to save a file named after your input with a .types.ts extension (e.g. users.json becomes users.types.ts). The output is plain TypeScript source with one blank line between types — paste it straight into a .ts/.tsx file or a dedicated types.ts module.
What are the size and run limits?
JSON to TypeScript is a Pro tool gated by the shared codeGeneration quota. Free is 1 run per day; if the JSON root is an array, only the first 100 rows are sampled before generation. Pro is 5 runs/day with a 1,000-row sample cap; Pro Media is 50/day at 10,000 rows; Developer and Enterprise are unlimited. The file-size ceiling is 2 MB on free and 100 MB on paid tiers. The row cap only matters for array roots — a single deep object generates the same types regardless of tier because the shape, not the row count, drives the output.
Is my component prop data uploaded to a server?
No. Generation runs entirely in your browser via the converter in lib/json-to-typescript.ts — the JSON is parsed and walked client-side, and only an anonymous run counter is recorded when you're signed in. Component prop data never leaves the tab, which is why you can safely paste real responses that contain customer records, tokens, or internal identifiers.
Should I generate prop types or share types from the API layer instead?
If your component consumes an API you own, generating types once at the fetch boundary and importing them is cleaner than per-component prop types — see the API response types workflow. Use this prop-type flow for components fed by parent state, third-party data, or Storybook args where there's no central type to import. Either way, paste real data so the shape matches reality rather than drifting documentation.
Privacy first
Conversion runs locally in your browser. No file is uploaded — only metadata counters are saved for signed-in dashboard stats.