Type debt is not TypeScript's fault

TypeScript feels great early in a project. Editors know more, refactors are safer, and API fields are harder to mistype. As the project grows, new problems appear: any spreads, backend response types leak into deep components, form state becomes impossible to model, and props grow without boundaries.

The problem is not that TypeScript failed. The problem is that the project did not define type ownership.

Separate three kinds of types

API DTOs describe data at the network boundary. They should match backend responses and request bodies closely.

Domain models describe how the frontend wants to reason about business data. They may normalize nullable fields, convert status strings into enums, or prepare display-ready values.

View state belongs to a page or component: expanded rows, selected ids, draft inputs, validation messages, and temporary UI state. It should not pollute API types.

Convert at the boundary

A common mistake is passing raw API responses through the entire component tree. That makes every component depend on backend field names and nullability rules. When the API changes, the whole page breaks.

A better pattern is to convert API DTOs into frontend models at the API layer or page entry. Components depend on the model, not the raw response.

any needs an exit plan

Old projects cannot remove every any overnight. But every any should have a reason and a boundary. It may be acceptable around a legacy module, third-party library, or unknown JSON input. It should not flow into core business components.

If an any passes through three layers without being narrowed, it has become type debt.

Forms deserve their own types

Form drafts are often incomplete. They contain strings, empty values, and invalid intermediate state. They should not be treated as submit DTOs. Use one type for draft input, one for validated data, and one for the final API payload.

This makes validation clearer and prevents invalid data from reaching submit logic.

Tighten gradually

Pick one high-change module. Add DTOs, conversion functions, and frontend models. Require new APIs to have response types. Prevent new shared components from using raw any. Tighten compiler settings directory by directory.

Takeaway

TypeScript pays off when boundaries are clear. DTOs protect the network edge, models serve business logic, view state stays local, and any is gradually contained.