Move a JavaScript Codebase to TypeScript Without Stopping Delivery
An incremental migration, done as a scoped piece of work: strictness raised one flag at a time, the build green at every commit, and your team still shipping features while it happens.
What a Migration Is Actually Buying You
Not tidiness. Four specific, measurable improvements in how the team works.
Refactoring Becomes Possible
Renaming a field or changing a function signature stops being an exercise in grep and hope. The compiler finds every call site, including the one in the module nobody remembered.
A Whole Class of Bug Disappears
Undefined is not a function, a property read from the wrong shape, an optional field treated as guaranteed. These stop reaching production because they stop compiling.
Onboarding Gets Cheaper
Types are documentation that cannot go stale. A new developer can see what a function expects without reading three call sites and guessing at the shape of an object.
Contracts Stop Drifting
With shared types, or a schema that generates both, the API response and the code consuming it cannot quietly diverge for three sprints before anyone notices.
When It Is Not Worth It Yet
We would rather turn down a migration than take payment for one that will not pay you back.
If you are not sure which of these describes you, a code audit answers the question with evidence rather than opinion.
Incremental, Not a Rewrite
The two ways this gets done, and why only one of them tends to finish.
Big-Bang Conversion
A branch is created, the whole codebase is converted, and the team promises not to change too much on main in the meantime. They always change things on main.
- Nothing is verifiable until everything is done
- The branch diverges until merging costs more than the migration
- Feature work and migration work conflict constantly
- A quarter in, someone asks what this is still buying us
- Abandoned migrations leave a codebase in two dialects with no plan
Incremental Adoption
TypeScript is switched on with allowJs so both file types compile together, and modules move over one at a time in ordinary pull requests.
- The application works and deploys at every commit
checkJsstarts finding real bugs in files still written in JavaScript- Each change is small enough to review properly
- Benefit arrives from the first week, not at the end
- If priorities change, you stop at a coherent point rather than a broken one
There is one genuine exception: a small codebase, a quiet week and a team already fluent in TypeScript. Then a single pass is simply faster. It is a much narrower case than people assume.
The Strictness Ladder
Turning every flag on at once produces thousands of errors and a team that switches TypeScript back off. One rung at a time produces a codebase that gets stricter every month.
Compile Everything, Check Nothing
TypeScript added to the build with allowJs enabled and strictness off. No file changes yet. The only goal is that the existing application still builds, tests still pass and deployment is unaffected. This is the step people skip and the one that makes the rest safe.
Check the JavaScript You Already Have
Enable checkJs, either globally or file by file. The compiler starts inferring types from your existing JavaScript and reporting genuine mistakes without a single file being renamed. Teams are routinely surprised by what this alone finds.
Convert the Boundaries First
Shared utilities, API clients, data models and the types that describe your domain. Typing the edges makes every file that imports them better, including the ones still written in JavaScript, so the benefit compounds rather than arriving at the end.
Turn On noImplicitAny
The first flag that will produce real work. Every parameter with no inferable type has to be declared. Done after the boundaries are typed, most of it resolves itself; done first, it is thousands of errors at once and a demoralised team.
Turn On strictNullChecks
The flag that finds the most real bugs and takes the most effort, because it forces the codebase to be honest about every value that might be absent. This is where a migration earns most of its money, and where an incremental approach is worth its weight several times over.
Full Strict, Then Hold the Line
The remaining flags under strict, then lint rules that stop new implicit any values and unchecked assertions arriving. Without that last step the codebase quietly drifts back, and six months later you have strict mode and no actual strictness.
Dependencies, Escape Hatches and the Build
Three things that decide whether the migration is pleasant or miserable.
Typing What You Did Not Write
- Community definitions — if they exist, install them and move on. The cheapest rung on the ladder.
- A minimal declaration — for an untyped package, declare only the functions you actually call. This is usually a short file, not a description of the whole library.
- A typed wrapper — for a genuinely hostile dependency, put a thin module of your own in front of it so the mess lives in one file with a clean surface.
- A finding, not a task — a package with no types because it has been abandoned is a different problem, and one worth raising separately.
Keeping the Build Green
- Type checking added to CI from the first day, so nothing can get worse while the migration is in progress.
- Every pull request deployable. No long-lived migration branch, no "we will fix the types later" merges.
- Strictness raised only when the codebase is already clean under the next flag, which turns each rung into a small change rather than a reckoning.
- Type errors treated as build failures rather than warnings people learn to scroll past.
How We Run It
- Scoped and fixed-price, agreed after we have read the codebase rather than before.
- Done as pull requests into your repository, reviewed by your team, in your normal workflow.
- Your developers learn the patterns as it goes, because they are reviewing every change.
- Or hire a TypeScript developer to do it inside your team instead, if you would rather it never be a separate project at all.
Where any Is Fine and Where It Defeats the Point
A migration with no escape hatches never finishes. A migration that is mostly escape hatches was not worth doing.
A Staging Post With a Name
A file converted this week, with the awkward parts marked and a ticket against them. Deliberate, visible and temporary. Better still, unknown at the boundary, which forces a check before use rather than waving the value through.
Third-Party Edges
The response from a library with no types, or a legacy global that will never be modelled properly. Contained behind a typed wrapper, in one file, rather than spread through the code that calls it.
The Type Nobody Could Work Out
A genuinely intricate generic that took an afternoon and still fails. Sometimes the right call is to move on. It becomes a problem when it is the first response to difficulty rather than the last.
Silencing the Compiler
Assertions used to make an error go away, any on API responses and function parameters across the codebase, ignore comments as a house style. At that point you have paid for TypeScript and kept JavaScript, with extra syntax.
How Long It Realistically Takes
It scales with size and with how much you choose to convert, so these are ranges. Anyone quoting an exact figure before reading your code is guessing.
A Single Small Application
One team, one build, a few dozen modules. Usually a matter of weeks running alongside normal delivery, with strict mode reached at the end rather than being the first milestone.
A Mid-Sized Product
Several modules, a shared component library, more than one team touching it. A few months of part-time work, with most of the value arriving early once the shared types are in place.
A Large Monorepo
Many packages and several deployed applications. A programme measured in quarters, and one where the correct answer is usually to type the shared packages and the actively changing applications and leave the quiet corners alone.
The variable that moves these most is not the line count. It is how much of the codebase is genuinely dynamic — objects assembled at runtime, functions that return different shapes depending on an argument, data passed through without ever being described.
Where to Go From Here
Hire TypeScript Developers
If you would rather this happened inside your team than as a separate project with a start and an end.
Learn moreCode Audits
An evidence-based answer to whether a migration is the right thing to spend the next quarter on.
Learn moreLegacy Modernisation
When typing is only part of it and the build, the framework and the Node version all need attention too.
Learn moreTesting and Automation
Types catch shape errors, tests catch wrong answers. If you only have budget for one, this is often it.
Learn moreReact Ecosystem
Where typed schemas, forms and query caches meet, and where a migration pays off fastest on the frontend.
Learn moreRates
How developers are priced per month or per hour, and how fixed-scope work is quoted instead.
Learn moreQuestions About Migrating to TypeScript
Will the migration stop us shipping features?
It should not, and if it does then it is being done wrong. An incremental migration leaves the application working at every commit: JavaScript and TypeScript files coexist, the build stays green, and each merged change is small enough to review alongside normal work. The version that does stop feature delivery is the long-lived migration branch, which spends months diverging from main and then cannot be merged without a fortnight of conflict resolution. We do not work that way.
Do we have to convert the entire codebase?
No, and for a large codebase you probably should not plan to. The value is concentrated in the parts that change often, carry business rules or handle money and permissions. A stable module nobody has touched in two years gets very little from being typed and will cost real effort to convert. A sensible end state is a codebase where the actively developed core is typed and strict, new code is TypeScript by default, and a dwindling tail of quiet files is still JavaScript and checked loosely.
How long does a migration realistically take?
It scales with the size of the codebase and how much of it you choose to convert, so we give ranges rather than a number. A single small application with one team on it is usually a matter of weeks alongside normal delivery. A mid-sized product with several modules and a shared component library runs into a few months of part-time work. A large multi-package monorepo is a programme measured in quarters, and the right approach there is to type the shared packages and the actively changing applications first and leave the rest. Anyone who quotes you an exact figure before reading the code is guessing.
What do we do about dependencies with no type definitions?
There is a ladder of options and we take the cheapest rung that works. If community definitions exist, install them. If not, write a minimal declaration covering only the parts of the library you actually call — that is usually a small file, not a full description of the package. If the library is genuinely untypeable or actively hostile, wrap it in a thin module of your own with a typed surface, so the mess is contained in one file rather than spread across the codebase. And if a dependency has no types because it is abandoned, that is worth knowing separately, because the migration has just found you a different problem.
Is TypeScript worth it for a small codebase?
Sometimes not, and we will tell you when we think that is the case. A small application with one or two developers who know it well, changing slowly, with tests that pass, gets modest benefit for real cost. The same is true of a codebase you are likely to replace within the year, or a team with no appetite for the learning curve during a delivery crunch. TypeScript earns its place when several people touch the same code, when refactors are frequent, when onboarding happens regularly, or when runtime type errors are showing up in your incident log.
Tell Us What You Are Working With
Repository size, framework, build setup and how much of it changes in a normal month. We come back with a scoped plan and a fixed price, or with the reason not to bother yet.