How to typescript interface or zod schema from excel? when to use each
- Step 1Decide if the data is trusted — Is the Excel data internal/already-validated, or does it arrive from a user upload or external API? Trusted -> interface is enough. Untrusted -> you need Zod at the boundary.
- Step 2Convert the first sheet to JSON — Both generators take JSON, not
.xlsx.XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]])and grab one representative row object. - Step 3Generate a Zod schema (for boundaries) — Paste the row at /tool/json-to-zod. You get a
z.object()plus az.infer<>type alias — schema and type from one paste. - Step 4Or generate an interface (for trusted data) — Paste the same row at /tool/json-to-typescript for a pure
interfacewith no runtime cost — ideal for typing data you already validated upstream. - Step 5Prefer derive-the-type over generating both — If you generated Zod, do not also generate an interface. Write
type Row = z.infer<typeof schema>so the type always matches the validator. - Step 6Refine the Zod schema for real validation — Add
.optional(),.email(),.min(), and date coercions the generator cannot infer. An interface needs no such refinement because it never validates anything.
Interface vs Zod schema at a glance
Both describe the same Excel columns; only Zod acts at runtime.
| Aspect | TypeScript interface | Zod schema |
|---|---|---|
| When it checks | Compile time only | Runtime (per value) |
| Catches a bad upload | No — types are erased | Yes — safeParse returns success/error |
| Runtime cost | Zero (erased) | Small (validation per row) |
| Unknown columns | No enforcement | z.object() strips, z.strictObject() rejects |
| Gives you a TS type | It is the type | z.infer<typeof schema> |
| JAD generator | /tool/json-to-typescript | /tool/json-to-zod |
| Best for | Trusted/internal data | Untrusted boundaries (uploads, APIs) |
What each generator emits from one JSON row
Same sample, two outputs. Inference is structural in both; neither detects dates or adds optionals.
| Sample row | json-to-typescript | json-to-zod |
|---|---|---|
{ name: "Sue", age: 30 } | interface Schema { name: string; age: number } | z.object({ name: z.string(), age: z.number().int() }) |
active: true | active: boolean | active: z.boolean() |
tags: ["a"] | tags: string[] | tags: z.array(z.string()) |
created: "2026-06-12" | created: string | created: z.string() (add .date()) |
Decision matrix
Pick by where the data crosses a trust boundary.
| Scenario | Recommended | Why |
|---|---|---|
| User uploads an Excel template | Zod schema | Untrusted bytes must be validated |
| Typing an internal config object | Interface | No runtime check needed |
| API request body from a 3rd party | Zod schema | Reject malformed input with 400 |
| Typed access to already-validated rows | z.infer<> of your Zod | One source of truth |
| Need both Zod + interface from Excel | tRPC Router Builder | Emits both from the sheet directly |
Cookbook
Concrete decisions: each example shows the JSON sample, the two possible outputs, and which one to ship for that situation.
Untrusted upload -> Zod, not interface
A customer uploads an order sheet. An interface would compile but let a string slip into qty at runtime; Zod rejects it.
Sample: { orderId: "X1", qty: 3 }
Interface (compile-time only, NO runtime guard):
interface OrderRow { orderId: string; qty: number }
Zod (runtime guard — ship this for uploads):
export const orderRow = z.object({
orderId: z.string(),
qty: z.number().int()
});
orderRow.safeParse({ orderId: "X1", qty: "oops" }) -> success:falseDerive the type — don't hand-maintain both
Generate Zod, then get the interface-equivalent for free. They can never drift apart.
export const orderRow = z.object({
orderId: z.string(),
qty: z.number().int()
});
// instead of a separate interface:
export type OrderRow = z.infer<typeof orderRow>;
// OrderRow === { orderId: string; qty: number }Trusted internal config -> interface is enough
For a build-time config object you control, the interface gives types with zero runtime overhead.
Sample: { featureFlag: "beta", maxSeats: 50 }
Generate at /tool/json-to-typescript:
interface AppConfig {
featureFlag: string;
maxSeats: number;
}
// no Zod needed — you author this config, it is trustedStrict template enforcement (Zod only)
An interface cannot reject an unexpected column; z.strictObject() can.
strictObjects: true ->
export const orderRow = z.strictObject({
orderId: z.string(),
qty: z.number().int()
});
orderRow.safeParse({ orderId: "X1", qty: 3, legacyCol: "x" })
-> success:false, 'Unrecognized key: legacyCol'
No interface can do this.Both artifacts from Excel in one pass
When you genuinely want a standalone interface AND a Zod schema from the sheet, use the Excel-native tRPC Router Builder.
/excel-tools/excel-trpc-router on an Orders sheet emits:
export const OrdersSchema = z.object({
orderId: z.string().optional(),
qty: z.number().optional()
});
export interface Orders {
orderId?: string;
qty?: number;
}
// plus list/getById/create/update/delete proceduresEdge cases and what actually happens
Using an interface to validate an upload
No runtime guardTypeScript interfaces are erased at compilation — they cannot catch a missing column or wrong type in an actual uploaded file. For any untrusted Excel input, use Zod's safeParse.
Maintaining a hand-written interface alongside Zod
Drift riskTwo definitions for one shape drift apart over time. Generate the Zod schema and derive the type with z.infer<typeof schema> so there is a single source of truth.
Uploading the .xlsx to either generator
Not acceptedBoth Excel-to-TypeScript and Excel-to-Zod redirect to JSON-sample tools that take .json/.ndjson/.jsonl/.txt. Convert the sheet to JSON first, or use the tRPC Router Builder for direct .xlsx.
Expecting Zod to add .optional() like the tRPC builder
By designThe JSON to Zod generator never adds .optional() — it reads one sample value. The Excel-native tRPC Router Builder does wrap every field in .optional() because it samples rows; the JSON-to-TypeScript generator likewise infers from one sample.
Date columns typed as string in both
ExpectedNeither generator detects dates: the interface gets string, the Zod schema z.string(). Refine the Zod side with .date()/.coerce.date(); the interface stays string since it never validates.
Choosing strict objects for a typed-but-loose need
Mismatchz.strictObject() rejects extra columns, which an interface never does. If you want interface-like permissiveness with runtime checks, use the default z.object() (strips extras) rather than strict mode.
Invalid JSON sample
Parse errorBoth generators JSON.parse the input; a malformed paste throws and produces nothing. Stringify a clean row with JSON.stringify(rows[0], null, 2).
Large sample pasted
413 limitBoth are Pro tools with a 2 MB free cap. One representative row is all either generator needs — never paste the whole workbook.
Performance worry about runtime validation
NegligibleZod validation per row is microseconds; the cost is rarely measurable next to parsing the file or hitting the DB. The safety on an untrusted boundary far outweighs it. Use interfaces only where you have already validated.
Nested data from a flattened sheet
One levelsheet_to_json flattens to a single level, so both outputs are flat. If you need nested objects, nest the JSON manually before pasting; both generators recurse into nested JSON correctly.
Frequently asked questions
When should I generate a Zod schema vs a TypeScript interface from Excel?
Use Zod at any boundary where untrusted Excel data enters (uploads, external APIs) because it validates at runtime. Use a plain interface for data you already trust (internal config) where compile-time types are enough and runtime cost should be zero.
Can I use both for the same Excel structure?
You can, but the clean pattern is to generate the Zod schema and derive the type with type Row = z.infer<typeof schema>. That gives you a runtime validator and a compile-time type from one source, with no drift.
Why can't a TypeScript interface validate uploads?
TypeScript types are erased during compilation — they exist only at build time. At runtime there is no interface to check against, so a wrong type or missing column in a real file passes straight through.
Do both tools read my .xlsx directly?
No. Excel-to-TypeScript redirects to /tool/json-to-typescript and Excel-to-Zod to /tool/json-to-zod; both take a JSON sample. Convert the sheet to JSON first, or use the tRPC Router Builder for direct Excel input.
Does the Zod generator add .optional() for empty columns?
No. It infers from one sample value and never adds .optional(). Add it yourself, or use the tRPC Router Builder, which marks every field .optional() after sampling rows.
How are dates typed?
As strings in both: string for the interface, z.string() for Zod (no .datetime()/.date()). Add the refinement on the Zod side for real validation.
Is there a runtime performance penalty for Zod?
Minimal — per-row validation is microseconds and dwarfed by file parsing and DB calls. On an untrusted boundary the safety is well worth it. Skip Zod only where data is already validated.
What about unexpected columns?
An interface ignores them. Zod's default z.object() strips them; z.strictObject() (set strictObjects: true) rejects them. Pick strict mode when an unexpected column should be a hard error.
Can I get both a Zod schema and an interface from Excel at once?
Yes — the tRPC Router Builder emits a Zod schema, a TypeScript interface, and CRUD procedures from your sheet headers in a single pass.
Is my Excel data uploaded during generation?
No. Both generators run entirely in the browser; your JSON sample never reaches a server. Only an anonymous run counter is logged for dashboard stats.
What are the size limits?
Both are Pro tools with a 2 MB free file cap. Since you only paste one representative row, the limit rarely matters.
Do the other dev-bridge tools follow the same pattern?
Yes. Excel-to-SQL redirects to /tool/json-to-sql and Excel-to-Markdown to /tool/json-to-markdown — all JSON-sample generators. For Excel-native code generation see Python dict generator.
Privacy first
Every JAD Excel tool runs entirely in your browser using SheetJS and ExcelJS. Your spreadsheets, formulas, and data never leave your device — verified by zero outbound network requests during processing.