How to generate typescript types for next.js server component data
- Step 1Log the fetch / query result — In the Server Component or data helper, temporarily add
console.log(JSON.stringify(data, null, 2))after theawait fetch(...), ORM query, or CMS call. Copy the full logged object from the server terminal. Use a request that returns every field populated. - Step 2Paste and name the data type — Drop the JSON in and set 'Root type name' to the page data type, e.g.
PageDataorPostDetail. Nested objects auto-name from their keys. - Step 3Choose output style — Keep 'Use interface' on. Keep 'Export all types' on if the type will be imported by a Client Component or
generateMetadata; turn it off for a type used only within the file. - Step 4Leave optional off — Server data is usually a strict shape per route — leave 'Optional properties' off. Toggle it on only for a loose draft; it marks every field
?and won't detect which fields the source omits. - Step 5Generate and place — Click 'Generate Types', then Copy or Download .ts into a co-located
types.tsorlib/types.ts. - Step 6Type the helper and the boundary — Annotate the fetcher:
async function getPageData(): Promise<PageData> { const r = await fetch('...', { next: { revalidate: 60 } }); return r.json(); }. When passing to a Client Component, pass only serializable fields; the generated type already reflects serialized forms (dates asstring). Widen anyfield: nullplaceholders by hand.
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 Server Component data in, the verbatim TypeScript the generator emits out, and the App Router wiring you do next.
Type a data-fetching helper
ExampleA CMS/page response becomes the return type of your getPageData() helper, and inference carries into the component's JSX.
Input (logged fetch result):
{ "title": "Hello", "author": { "name": "Ada" }, "publishedAt": "2026-06-10T00:00:00Z" }
Output (rootName: PageData):
export interface PageData {
title: string;
author: Author;
publishedAt: string;
}
export interface Author {
name: string;
}
// async function getPageData(): Promise<PageData> { ... }Dates are already strings at the boundary
ExampleAn ORM may hand you a Date, but once logged via JSON it's an ISO string — and the generated string type is exactly what a Client Component receives after serialization.
Server query (in memory): { createdAt: Date }
Logged as JSON: { "createdAt": "2026-06-10T12:00:00.000Z" }
Generated type:
export interface Row {
createdAt: string; // correct for the serialized Server→Client prop
}
// new Date(row.createdAt) inside the Client Component if you need a Date.List page: typed item array
ExampleA posts index returns an array; set a descriptive root and get a single item interface to annotate the list.
Input (GET posts):
[ { "slug": "a", "title": "A" }, { "slug": "b", "title": "B" } ]
Output (rootName: Post):
export interface PostItem {
slug: string;
title: string;
}
// async function getPosts(): Promise<PostItem[]> { ... }Nullable CMS field needs widening
ExampleAn unset CMS field returns null, generating the literal null; widen to the real nullable type by hand.
Input:
{ "title": "Hello", "coverImage": null }
Output (verbatim):
export interface Root {
title: string;
coverImage: null;
}
Hand-edit:
export interface PageData {
title: string;
coverImage: string | null; // image URL or null
}params/searchParams come from Next.js, not this tool
ExampleGenerate types for the data, not for route params — Next.js provides those built-in. Note App Router params are async (Promise) in current versions.
// Do NOT generate this — write it from Next.js types:
type Props = {
params: Promise<{ slug: string }>;
searchParams: Promise<{ [k: string]: string | string[] | undefined }>;
};
// DO generate the data shape:
export interface PageData { /* from your fetch result */ }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.
Date typed as string
Correct for serializationJSON has no Date type, so timestamps become string. Because Server→Client props are JSON-serialized, a Date becomes an ISO string at the boundary anyway — the string type is the accurate prop type. Call new Date(value) in the Client Component if you need a Date object.
params/searchParams not generated
Out of scopeRoute params and searchParams types come from Next.js, not from your data — and in current App Router versions they're Promise-wrapped. Write those by hand and use this tool only for the fetch/query result types.
Array result sampled from first element
By designA list result infers its item interface from the first element. Keys exclusive to later items are missing — reorder so the most complete record is first, or paste a merged superset object before generating.
Non-serializable values can't appear
Not representableFunctions, class instances, undefined, and Map/Set aren't JSON, so they never reach the generated type. That's aligned with Server→Client rules, which forbid passing non-serializable props anyway — keep your prop interface to the serializable subset.
Nullable field typed as `null`
ExpectedAn unset CMS/database field returning null yields the literal null type. Widen field: null to T | null by hand, or capture a record where the field is populated.
BigInt / Decimal from the ORM
Upstream coercionPrisma BigInt/Decimal aren't valid JSON and throw on JSON.stringify unless converted first. Whatever you convert them to (string is common) is what the sample contains and what gets typed. Decide the wire form before logging.
Duplicate nested type names collide
First write winsIf author and editor both contain a profile object of different shapes, only the first Profile interface is emitted and both reference it. Rename one key, or generate the branches separately.
Empty array field
unknown[]An empty tags: [] becomes tags: unknown[]. Replace with the real element type or capture a response where the array is populated.
Large list hits the row sample cap
SampledWhen the root is an array, the tool caps the sample by tier (100 free / 1,000 Pro / 10,000 Pro Media rows) before generating. The item shape is unaffected — sampling fewer rows still produces the same item interface from the first element.
Invalid JSON pasted
Parse errorInput runs through JSON.parse after trimming. Server logs sometimes wrap the object in extra text or color codes — isolate the pure JSON, or repair with json-format-fixer, before regenerating.
Frequently asked questions
How do I type params and searchParams in a dynamic route?
Those come from Next.js, not from this tool. Write them by hand — and note that in current App Router versions they're async: type Props = { params: Promise<{ slug: string }>; searchParams: Promise<{ [k: string]: string | string[] | undefined }> }, then await props.params. Use this generator only for your fetch/query result shapes, which Next.js can't type for you.
Can I use Date objects in the generated types for Server Components?
Not at the Server→Client boundary. When you pass props to a Client Component they're JSON-serialized, so a Date becomes an ISO string. The generator already emits string for date values (JSON dates are strings), which is the correct serialized type. Inside a Server Component you can work with Dates freely; just type the props the Client receives as string and rehydrate with new Date(...) if needed.
Why didn't my functions or class instances show up in the type?
They aren't JSON-serializable, so they can't appear in a JSON.stringify-ed sample and the generator never emits them. This matches Next.js rules — you can't pass functions, class instances, or undefined as Server→Client props anyway. Keep the generated interface as your serializable prop contract and add behavior on the Client side.
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.
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.
My Prisma query has BigInt/Decimal fields — what type do I get?
Those aren't valid JSON and JSON.stringify throws on a raw BigInt, so you must convert them before logging (commonly to string or number). Whatever you convert to is what the sample contains and what the generator types. Decide the serialized form deliberately — string preserves precision for big integers and money, where number can lose it.
Does the generated type work with fetch caching and revalidation?
Yes — types are orthogonal to caching. The interface describes the response body; next: { revalidate }, cache: 'force-cache', and unstable_cache don't change the shape, so the same generated type annotates the helper regardless of caching strategy. Annotate the fetcher's return as Promise<PageData> and the cache config lives separately in the fetch options.
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.
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 Server Component 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. Server Component data never leaves the tab, which is why you can safely paste real responses that contain customer records, tokens, or internal identifiers.
Privacy first
Conversion runs locally in your browser. No file is uploaded — only metadata counters are saved for signed-in dashboard stats.