Every time you call an API in TypeScript and write as any, you are trading away type safety for convenience. The fix is straightforward: define an interface that matches the JSON response structure. But for large, nested payloads, writing those interfaces by hand is tedious and error-prone.

This guide explains how JSON maps to TypeScript types, how to use an automatic generator to bootstrap your interfaces, and how to refine the output for production use.

Why Type Your API Responses?

Without explicit types, TypeScript loses its two biggest advantages: compile-time error detection and IDE autocomplete.

// ❌ any disables all type checking
const data: any = await fetch('/api/user').then(r => r.json());
console.log(data.nmae); // Typo — no error, fails silently at runtime

// ✅ Typed response — typos caught at compile time
interface User { id: number; name: string; email: string; }
const data = await fetch('/api/user').then(r => r.json()) as User;
console.log(data.nmae); // Error: Property 'nmae' does not exist on type 'User'

Beyond error detection, typed responses give you autocomplete on every field — no more switching to Postman to remember whether it is createdAt or created_at.

How JSON Maps to TypeScript

JSON ValueTypeScript Type
stringstring
number (int or float)number
true / falseboolean
nullnull or optional ?
[ ... ] (array)Type[]
{ ... } (object)Nested interface

Simple Object

// Input JSON
{
  "id": 1,
  "name": "Alice",
  "email": "alice@example.com",
  "isActive": true,
  "score": 98.5
}

// Generated TypeScript
interface User {
  id: number;
  name: string;
  email: string;
  isActive: boolean;
  score: number;
}

Nested Objects

// Input JSON
{
  "id": 1,
  "name": "Alice",
  "address": {
    "street": "123 Main St",
    "city": "Portland",
    "zip": "97201"
  },
  "tags": ["premium", "verified"]
}

// Generated TypeScript
interface Address {
  street: string;
  city: string;
  zip: string;
}

interface User {
  id: number;
  name: string;
  address: Address;
  tags: string[];
}

Handling null Values

// Input JSON
{
  "id": 1,
  "name": "Alice",
  "deletedAt": null,
  "bio": null
}

// Option 1: nullable union
interface User {
  id: number;
  name: string;
  deletedAt: string | null;
  bio: string | null;
}

// Option 2: optional (null → ?)
interface User {
  id: number;
  name: string;
  deletedAt?: string;
  bio?: string;
}

Using the JSON to TypeScript Generator

The JSON to TypeScript Interface Generator converts any JSON into TypeScript interfaces in real time. Paste a JSON sample and choose your options:

  • Interface vs Type alias: generates interface Foo { ... } or type Foo = { ... }
  • readonly: adds readonly to every property — useful for immutable API response objects
  • null → optional: converts null values to optional ? instead of | null
  • Nested interfaces: extracts nested objects into named interfaces for readability
  • Sample presets: User, Product, and Order samples to explore the tool instantly

Practical Patterns

Generic API Response Wrapper

// Most APIs wrap responses in a common envelope
{
  "success": true,
  "data": { ... },
  "meta": { "total": 100, "page": 1, "perPage": 20 }
}

// After generation, add generics to make it reusable
interface Meta {
  total: number;
  page: number;
  perPage: number;
}

interface ApiResponse {
  success: boolean;
  data: T;
  meta: Meta;
}

// Usage
const res = await fetch('/api/users').then(r => r.json()) as ApiResponse;

Combining with Zod for Runtime Validation

import { z } from 'zod';

// Use the generator to sketch the shape, then write the Zod schema
const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
  isActive: z.boolean(),
});

type User = z.infer; // TypeScript type derived from schema

// Fetch + validate + type in one step
const data = UserSchema.parse(await fetch('/api/user').then(r => r.json()));

Typing localStorage Data

interface AppSettings {
  theme: 'light' | 'dark';
  language: string;
  notifications: boolean;
}

function loadSettings(): AppSettings | null {
  const raw = localStorage.getItem('settings');
  if (!raw) return null;
  return JSON.parse(raw) as AppSettings;
}

Refining Auto-Generated Types

Auto-generated interfaces are a starting point, not a final product. A few common improvements:

  • Empty arrays: [] may be inferred as never[] or unknown[] — replace with the actual element type
  • Date strings: ISO date strings are typed as string — add a comment or parse to Date
  • Union types: narrow status: string to status: 'active' | 'inactive' | 'banned' for exhaustive checks
  • Interface names: rename generic names (RootObject, Item) to domain-meaningful names
  • Generics: extract pagination wrappers or API envelopes into reusable generic types

FAQ

Should I use interface or type?
Both work for object shapes. The TypeScript team recommends interface for object types that may be extended, and type for union types, intersection types, and mapped types. Pick one and be consistent within a codebase.
What happens when the array in JSON is empty?
An empty array [] has no elements to infer the type from, so tools may output never[] or unknown[]. Use a representative API response that contains at least one array element for best results.
When should I use Zod instead of plain interfaces?
Use Zod when you need runtime validation — i.e., when the data comes from an external source (API, user input, localStorage) and you cannot guarantee its shape at compile time. For internal data you fully control, plain interfaces are sufficient and have zero runtime cost.
How do I type an API response in fetch without casting?
fetch returns Response and .json() returns Promise<any>. You need to cast or validate. A common pattern: const data = (await resp.json()) as User. For safety, validate with Zod before casting.
What about OpenAPI / Swagger?
If your API has an OpenAPI spec, tools like openapi-typescript can generate TypeScript types directly from the spec — more reliable than inferring from a single JSON sample. The JSON-to-TypeScript approach is best when you only have a response payload (no spec), or for rapid prototyping.

Summary

  • Typing API responses unlocks TypeScript's full power: compile-time error detection and IDE autocomplete on every field.
  • JSON maps directly to TypeScript: strings → string, numbers → number, objects → interface, arrays → T[].
  • Use the JSON to TypeScript Generator to bootstrap interfaces from any JSON payload.
  • Refine auto-generated types: narrow string unions, name interfaces meaningfully, and add generics for reusable wrappers.
  • For runtime validation, pair TypeScript interfaces with Zod — derive the type from the schema using z.infer.