the pragmatic migration, no big bang
Rewriting an entire grown JavaScript React project to TypeScript sounds like weeks of standstill. Whoever introduces TypeScript gradually enables allowJs, migrates file by file, and eventually reaches strict mode without ever interrupting feature work.
Table of Contents
- 1. Why introduce TypeScript gradually instead of a big bang
- 2. Base setup with allowJs and checkJs
- 3. Using JSDoc types before the actual migration
- 4. Order: which file becomes .tsx first
- 5. Migrating the first component to TypeScript
- 6. Using any deliberately instead of banning it
- 7. Custom hooks and generic types
- 8. Enabling strict mode as the last step
- 9. Migration phases compared directly
- 10. Summary
- 11. FAQ
1. Why introduce TypeScript gradually instead of a big bang
A complete rewrite of a grown React project from JavaScript to TypeScript in a single pull request is not a realistic option for most teams. Whoever wants to introduce TypeScript gradually instead accepts a transition period in which JavaScript and TypeScript files exist side by side and get compiled together, without feature development having to pause.
The TypeScript compiler was built exactly for this scenario. With the allowJs option, tsc processes both .ts and .js files in the same project, and with checkJs, even JavaScript files get type checked without needing to be renamed. This combination turns a risky, months long migration into a continuous background project that runs alongside normal development.
The economic reason for the gradual migration is just as important as the technical one: a big bang rewrite ties up the entire team for weeks without delivering any new business value in that time. Whoever introduces TypeScript gradually instead delivers measurable progress every week, while features keep being developed as usual.
2. Base setup with allowJs and checkJs
The first concrete step is a tsconfig.json deliberately configured to be permissive. Strict mode and many other strict options stay disabled at first, because they would immediately produce hundreds of errors in unchanged JavaScript code. The goal of this first phase is merely to get the compiler to run error free at all.
// tsconfig.json: deliberately permissive starting point
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"jsx": "react-jsx",
"allowJs": true,
"checkJs": false,
"strict": false,
"noImplicitAny": false,
"skipLibCheck": true,
"esModuleInterop": true,
"moduleResolution": "bundler"
},
"include": ["src"]
}
Whoever introduces TypeScript gradually should leave checkJs disabled at first and only enable it once the roughest problems in individual files have been fixed deliberately. Enabling checkJs too early floods the IDE with warnings for code that isn't even part of the current migration wave yet, and demotivates the team unnecessarily.
3. Using JSDoc types before the actual migration
An often overlooked intermediate step before actually renaming files while introducing TypeScript gradually is using JSDoc type annotations in plain JavaScript. The TypeScript compiler already understands @param and @returns comments and uses them for autocompletion and type checking, with no .ts file extension required.
// utils.js: JSDoc types give the TypeScript compiler information
// without renaming the file to .ts yet
/**
* @param {number} price
* @param {number} taxRate
* @returns {number}
*/
export function calculateGrossPrice(price, taxRate) {
return price * (1 + taxRate);
}
/**
* @typedef {Object} CartItem
* @property {string} id
* @property {number} price
* @property {number} quantity
*/
/**
* @param {CartItem[]} items
* @returns {number}
*/
export function calculateCartTotal(items) {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
This intermediate step has a practical advantage: it costs almost nothing, can be done incidentally alongside any code change that was going to happen anyway, and makes function signatures safer well before the actual file migration. Teams that introduce TypeScript gradually gain measurable benefit this way weeks before the first file even gets renamed.
4. Order: which file becomes .tsx first
For the order of the actual migration, a clear rule applies: utility functions and plain data structures without UI first, React components with complex props interfaces and external API calls last. A utility function like formatCurrency has few dependencies and can be typed in minutes, while a form component with React Hook Form and several nested fields needs considerably more care.
A second criterion is change frequency. Files constantly edited by several developers at once should be migrated early, because TypeScript provides the greatest benefit there: merge conflicts caused by incorrect data types become visible at compile time instead of only at runtime.
5. Migrating the first component to TypeScript
When porting the first React component, Button.js becomes Button.tsx, and props get an explicit interface instead of an implicit object shape. Whoever introduces TypeScript gradually should deliberately start here with a simple, presentational component, not a container component that merges several data sources.
// BEFORE: Button.js, props shape only documented in comments or nowhere at all
export function Button({ label, variant, onClick, disabled }) {
return (
<button className={`btn btn-${variant}`} onClick={onClick} disabled={disabled}>
{label}
</button>
);
}
// AFTER: Button.tsx, props shape enforced by the compiler
type ButtonVariant = 'primary' | 'secondary' | 'danger';
interface ButtonProps {
label: string;
variant: ButtonVariant;
onClick: () => void;
disabled?: boolean;
}
export function Button({ label, variant, onClick, disabled = false }: ButtonProps) {
return (
<button className={`btn btn-${variant}`} onClick={onClick} disabled={disabled}>
{label}
</button>
);
}
6. Using any deliberately instead of banning it
A common misunderstanding among teams introducing TypeScript gradually: any gets strictly banned from the start, which needlessly slows down the migration. In the early phase, a deliberately placed any at a boundary to not yet migrated code is the right choice, as long as it sits at a clearly visible spot and doesn't sneak in through missing type annotations.
The difference between implicit and explicit any is crucial. An implicit any arising from missing typing hides itself and is easily overlooked. An explicit data: any with a // TODO: type this once the API client is migrated comment is a deliberate, traceable decision that can be resolved later in a targeted way. A grep -r ": any" across the project shows the current state of remaining spots at any time.
7. Custom hooks and generic types
Custom hooks benefit especially strongly from TypeScript, because generic types bind the return value precisely to the call site. A hook like useFetch<T> returns different data types depending on usage, which in plain JavaScript could only be documented via comments and is checked directly by the compiler in TypeScript.
Whoever introduces TypeScript gradually and migrates custom hooks should use generic type parameters instead of writing a separate, nearly identical hook variant for every use case. This reduces code duplication and makes type safety immediately visible at every call site, without needing to maintain documentation manually.
8. Enabling strict mode as the last step
Only once the majority of the codebase is migrated should strict: true be enabled in tsconfig.json. This step activates, among others, strictNullChecks, which typically produces the most new errors, because null and undefined must be handled explicitly from this point on instead of being silently accepted.
A pragmatic intermediate step is enabling strict options individually rather than all at once: first noImplicitAny, then strictNullChecks, only afterward the remaining options. This keeps the error list manageable after each activation, instead of producing hundreds of new compiler errors at once with a single big switch.
9. Migration phases compared directly
The table below ranks the individual phases of introducing TypeScript gradually by effort and benefit.
| Phase | Effort | Benefit | Typical duration |
|---|---|---|---|
| allowJs setup | Very low | Compiler runs, no errors | A few hours |
| JSDoc types | Low | Autocompletion without renaming | Ongoing, incidental |
| Migrating utility functions | Medium | Safe core data structures | One to two weeks |
| Migrating components | High | Props contracts compiler checked | Several months |
| Enabling strict mode | High, one time | Maximum type safety | One to two weeks at the end |
The table shows that the early phases of the migration offer the best effort to benefit ratio. Whoever introduces TypeScript gradually should therefore deliberately start with the cheap phases and save the expensive strict mode step for the end, once most of the work is already done.
Mironsoft
React TypeScript migrations and type safety consulting
Still running a pure JavaScript React project without type safety?
We plan the gradual TypeScript introduction for your project, prioritize files by risk, and guide the path all the way to full strict mode.
Migration Setup
tsconfig.json with allowJs and a gradual strictness plan
Guided Migration
File by file port without interrupting feature work
Strict Mode Rollout
Controlled activation of individual strict options
10. Summary
Whoever wants to introduce TypeScript gradually starts with a permissive tsconfig.json using allowJs, uses JSDoc types as a cheap intermediate step, and then migrates utility functions before complex components. A deliberately placed any at clearly visible spots is not a problem during the migration, as long as it is documented and resolved later.
Custom hooks benefit especially from generic types, and strict mode only gets enabled gradually at the end, once most of the codebase is already typed. This order turns a risky, months long migration into a continuous background project that never interrupts feature work.
Introducing TypeScript Gradually: The Essentials
Starting point
allowJs and checkJs allow JavaScript and TypeScript side by side in the same project.
Order
Utility functions first, complex components with many dependencies last.
Use any deliberately
Explicit any with a TODO comment instead of hidden implicit any.
Strict mode last
Enable individual strict options one after another instead of all at once.