TypeScript before you scale
Types are treated as a tax that big teams pay. The mechanism that makes them useful is strongest on small teams changing their minds often.
The case for TypeScript is usually made on behalf of other people. Large teams need it. Codebases with many contributors need it. The implication is that a two-person project can skip it and add it later, once the mess justifies the ceremony.
That framing has the mechanism backwards. Types are not primarily a coordination tool for large groups. They are a way of writing down decisions so that changing them is cheap — and small teams change decisions constantly.
What it actually does for you
A type is a claim about the shape of your data, checked everywhere that data is used. The value is not that the compiler catches typos. It is that when a claim changes, you get a list of every place that assumed the old one.
Say a user has an optional avatar. You decide it should be required, with a generated default. In an untyped codebase you search for the word, find most of the uses, and discover the rest in production. In a typed one you change the declaration and the compiler hands you the complete list — including the component you forgot existed and the one written by someone who has since left.
The benefit is not catching mistakes. It is knowing the full extent of a change before you make it.
This is why small teams benefit early. A small team has no code it can afford to be afraid of. Every file is one someone will need to change next month, usually the person who did not write it, often a version of yourself who has forgotten the reasoning.
Why later is more expensive than it sounds
Adding types to an existing codebase is not the same job as writing them as you go. Going second means:
- The shapes have already drifted. The same concept exists in four slightly different forms, and no one remembers which differences are intentional.
- The honest type is often a wide one. Describing what the code really does means unions of nullable variants, which is accurate and not much help.
- The escape hatch is always available. Under deadline pressure,
anysilences the compiler and the migration stalls half-finished, which is the worst state to be in: the cost of the syntax without the benefit of the checking. - It competes with product work. Nobody schedules a migration sprint for a benefit that is hard to demonstrate in a demo.
Written from the start, none of that applies. You are not describing existing code — you are deciding what the shapes are while you still have the decision in your head.
Where it pays first, in practice
Boundaries
Anywhere data enters your app — an API response, a form, a URL parameter, a stored value — is where wrong assumptions live longest, because the failure surfaces far from the cause. Typing the boundary turns a mysterious runtime error deep in a component into a compile error at the edge.
States that are mutually exclusive
A request is loading, or it succeeded with data, or it failed with an error. Three booleans can express all eight combinations, most of which are nonsense, and every component then has to guess which combination it is in. A union of three variants makes the impossible states unrepresentable, and the compiler will not let you read data from the failed one.
Anything the design system depends on
Variant names, spacing scales, and theme tokens are decisions designers and developers share. A typed set means a removed variant shows up as errors at every call site instead of as a component that silently renders with no styles.
The honest costs
It is not free, and the usual pitch is dishonest about which parts are annoying.
- Third-party types vary in quality. A wrong or overly loose type definition is worse than none, because you trust it.
- Generics get difficult quickly. Writing a reusable typed component is a genuinely harder skill than writing the untyped version, and the error messages are not kind.
- There is a build step to keep fast. Typechecking gets slower as a project grows and needs attention as infrastructure.
- It checks shapes, not truth. A field typed as a string can still hold the wrong string. Types eliminate a category of bug, not bugs.
The last point is the one to hold on to. Types are not tests, and they do not verify behaviour. They make refactoring safe, which is a different and cumulative benefit: the projects that stay pleasant to work on are the ones where changing a decision does not require courage.
If you are starting something now
- Turn on
strictfrom the first commit. Enabling it later means fixing every null check at once, which is the migration you were trying to avoid. - Type the boundaries first — API responses, forms, stored data. That is most of the value for a fraction of the effort.
- Model exclusive states as unions rather than as several booleans.
- Treat
anyas a comment saying this is not finished, and make it rare enough to notice in review. - Run the typecheck in CI, not just in the editor, so a broken type cannot merge.
None of this requires a large team, a long timeline, or a migration plan. It requires writing the decision down at the moment you make it, which is the only moment it is cheap.
If you are weighing this up for a product you are about to build, tell us about it.
