Introducing TypeScript Gradually into React
AI generated
</>
{ }
React · TypeScript · Migration · Type Safety
Introducing TypeScript Gradually into React
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.

17 min read allowJs · JSDoc · strict mode TypeScript 5.x · React 19

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.

11. FAQ: Introducing TypeScript Gradually

1Do I need to migrate everything at once?
No, allowJs lets JS and TS coexist indefinitely.
2What does JSDoc give me upfront?
Type information for the compiler without renaming files, costs almost nothing.
3In what order should I migrate?
Utility functions first, complex components last.
4Is any allowed during migration?
Yes, explicit with a TODO comment, as long as not hidden implicitly.
5When should I enable strict mode?
At the end, gradually option by option instead of all at once.
6How do custom hooks benefit?
Generic types bind the return value precisely to the call site.
7Does feature work need to pause?
No, that's exactly the advantage of the gradual migration.
8Difference between allowJs and checkJs?
allowJs processes JS files, checkJs additionally checks them for type errors.
9How do I find remaining any spots?
grep -r ": any" in the src folder shows every explicit annotation.
10Worth it for small projects?
Often not for tiny solo projects, almost always with multiple developers.