Introduction
When you consume a REST API in TypeScript, you need types that match the JSON response structure. Writing these types manually is tedious and error-prone - one wrong field name and your types silently diverge from reality. This guide shows you how to generate TypeScript types from JSON data automatically, and how to add runtime validation with Zod for end-to-end type safety.
The Problem: Manual Types Drift
Consider a typical API response:
// API response from /api/users/123
{
"id": 1,
"name": "Alice",
"email": "[email protected]",
"isActive": true,
"address": {
"street": "123 Main St",
"city": "San Francisco",
"zipCode": "94105"
},
"roles": ["admin", "user"]
}Writing the TypeScript interface manually:
interface User {
id: number;
name: string;
email: string;
isActive: boolean;
address: {
street: string;
city: string;
zipCode: string;
};
roles: string[];
}This works, but what happens when the API adds a phoneNumber field? Or changeszipCode to postalCode? Your TypeScript types silently drift from the actual API response. No compile error, no runtime error - just wrong types.
Solution 1: Auto-Generate TypeScript Types
Instead of writing types by hand, generate them from actual JSON data. You can use ourJSON to TypeScript toolor command-line tools.
Using the Online Tool
- Paste your API JSON response into the JSON to TypeScript tool
- Set the interface name (e.g., User, Product, Order)
- Choose TypeScript interface or type
- Click Generate and copy the output
Using quicktype (CLI)
# Install quicktype npm install -g quicktype # Generate TypeScript from a JSON file quicktype input.json -o user.ts --lang ts # Generate from a URL quicktype https://api.example.com/users/1 -o user.ts --lang ts
Solution 2: Runtime Validation with Zod
TypeScript types only exist at compile time. If an API returns unexpected data, your types won't catch it at runtime. Zod solves this by validating data at runtime while inferring TypeScript types automatically.
import { z } from "zod";
// Define the schema (also generates TypeScript types)
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
isActive: z.boolean(),
address: z.object({
street: z.string(),
city: z.string(),
zipCode: z.string(),
}),
roles: z.array(z.string()),
});
// Derive TypeScript type from the schema
type User = z.infer<typeof UserSchema>;
// Use it to validate API responses
async function fetchUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
// This throws if the data doesn't match the schema
return UserSchema.parse(data);
}With Zod, you get both compile-time types and runtime validation. If the API changes its response format, UserSchema.parse() will throw a clear error instead of silently accepting wrong data.
Best Practices
- Always validate API responses - Never trust external data. Use Zod schemas at API boundaries.
- Use z.infer for types - Define schemas once, derive types automatically. Single source of truth.
- Make optional fields explicit - Use
z.optional()orz.nullable()for fields that may be missing. - Generate types from real data - Use actual API responses, not imagined structures.
- Regenerate when APIs change - When the API updates, regenerate types and check for differences.
- Use z.coerce for form data - HTML forms and query params return strings.
z.coerce.number()converts automatically.
Common Patterns
Optional and Nullable Fields
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email().optional(), // may be undefined
phone: z.string().nullable(), // may be null
avatar: z.string().url().optional(), // may be undefined
});Discriminated Unions
const SuccessResponse = z.object({
status: z.literal("success"),
data: z.array(UserSchema),
});
const ErrorResponse = z.object({
status: z.literal("error"),
message: z.string(),
code: z.number(),
});
const ApiResponse = z.discriminatedUnion("status", [
SuccessResponse,
ErrorResponse,
]);Transform API Data
const UserSchema = z.object({
id: z.number(),
name: z.string(),
created_at: z.string().transform(str => new Date(str)),
is_active: z.boolean().transform(val => val ? "Active" : "Inactive"),
});
type User = z.infer<typeof UserSchema>;
// created_at is now Date, is_active is now "Active" | "Inactive"Tools for Generating TypeScript from JSON
| Tool | Type | Zod Support | Best For |
|---|---|---|---|
| ByteJSON Tool | Online (free) | Yes | Quick one-off generation |
| quicktype | CLI / Online | No | Multi-language generation |
| json-schema-to-zod | CLI / Library | Yes (output) | From JSON Schema to Zod |
| openapi-typescript | CLI | No | From OpenAPI specs |
Key Takeaways
- Never manually write TypeScript types for API responses - generate them from real data
- TypeScript types alone don't protect you at runtime - add Zod validation at API boundaries
- Use
z.infer<typeof Schema>to derive types from Zod schemas (single source of truth) - Regenerate types when APIs change to catch breaking changes early
- Start with our JSON to TypeScript tool for instant type generation