TypeScript: the gradual conversion
No big bang, no migration sprint. allowJs on day one, strictness ratcheted flag by flag, and an entire category of production bug quietly retired.
We put off TypeScript for years, and the excuse was always the same: "we'll adopt it in the next project." The next project never comes. What finally worked was giving up on the big bang and converting the codebase we actually live in — gradually, file by file, while shipping features the whole time.
The gradual path that worked
- Turn it on without turning it on.
allowJs: true, zero errors required. The codebase compiles day one; nothing is blocked. - New files are .ts, touched files get converted. No dedicated migration sprints — conversion rides along with normal work, so it never has to be justified to a roadmap.
- Strictness ratchets one flag at a time.
noImplicitAnyfirst,strictNullCheckslast — that one is the real war, because it audits every assumption you ever made about data being present.
Types are the documentation that can't drift and the tests you don't have to run.
What actually improved
The bugs TypeScript catches are rarely deep — they're the shallow ones that used to reach staging anyway: the renamed field, the function whose argument order changed, the API response that's null on Tuesdays. Catching those at edit time compounds. And the quiet win nobody advertises: refactoring stopped being frightening. Rename a type and the compiler hands you the complete, honest list of everything that breaks.
Discipline note: any is a loan, not a fix. We allow it with a comment and track the count in CI — the number must go down, never up. Two years in, our incident review tags show an entire category of production bug simply retired. That's what a type system is for.