jsontoschema
All posts
typescripttypesworkflowrefactoring

Replacing any with Generated Types in Legacy Code

Every legacy TypeScript codebase has the same problem: any everywhere. API responses typed as any. Config objects typed as any. Parsed JSON from files, databases, or message queues, all any. The codebase compiles, but TypeScript is doing none of the work it was designed for.

Replacing any by hand means reading each API response, understanding its shape, and writing interfaces manually. For a codebase with dozens of endpoints, that is days of work. There is a faster way: generate the interfaces from the actual data.

Find the worst offenders

Start by identifying where any causes the most pain. Run a quick search:

grep -rn ": any" src/ --include="*.ts" | wc -l

Sort the results by file. The files with the most any annotations are your targets. Focus on API client modules, data access layers, and utility functions that parse JSON. These are the places where a missing type causes bugs to propagate.

Capture real responses

For each any-typed API call, capture the actual response. If the service is running locally, use curl:

curl -s http://localhost:3000/api/orders/123 > fixtures/order.json

For database queries, log the result to a file during a test run. For message queue consumers, capture a sample message. The goal is a real JSON payload that represents the shape your code already handles.

Generate interfaces from the data

Pass each fixture through the converter to get a TypeScript interface:

npx @maisondigital/jsontoschema --typescript < fixtures/order.json

The output looks like this:

interface Root {
  id: number;
  status: string;
  customer: Customer;
  items: Item[];
  shipping_address: ShippingAddress;
  created_at: string;
  discount?: number | null;
}

interface Customer {
  id: number;
  name: string;
  email: string;
}

interface Item {
  sku: string;
  quantity: number;
  price: number;
  notes?: string;
}

interface ShippingAddress {
  line1: string;
  line2?: string;
  city: string;
  postal_code: string;
  country: string;
}

Fields that are missing or null in some array items are marked optional. Nested objects get their own named interfaces. This is not a guess at what the API might return. It is the exact shape of what it did return.

Replace incrementally

Do not try to fix every any in one commit. Pick one module, swap in the generated types, and fix the compiler errors that surface. Those errors are the point. Each one is a place where the code assumed a shape that may not match reality.

Rename Root to something meaningful like OrderResponse. Drop the interfaces into a types/ directory or colocate them with the module that uses them. Then update the fetch call:

const response = await fetch("/api/orders/123");
const order: OrderResponse = await response.json();

Now every field access on order is checked at compile time. Autocomplete works. Refactors that rename or remove fields surface immediately.

Handle multiple response shapes

Some endpoints return different shapes depending on query parameters or user roles. Capture two or three representative responses and generate interfaces from each. Compare the output. If the shapes differ only in optional fields, a single interface with optional properties covers both cases. If they diverge structurally, create a union type.

Keep types fresh

Generated types are a snapshot. If the API changes, regenerate from a fresh response. For critical endpoints, automate this: save a fixture in CI, regenerate the type, and fail the build if the interface changes unexpectedly. This pairs well with schema snapshot testing for a full structural safety net.

The payoff

A codebase with any sprinkled through its data layer is a codebase where TypeScript cannot help you. Replacing those annotations with generated types takes minutes per endpoint, not hours. The result is real type safety derived from real data, not hand-written interfaces that drift over time.

Paste a sample response into the converter and start with the endpoint that breaks most often.