Skip to main content
Technology & EngineeringMigration Patterns138 lines

Javascript to Typescript

Migrate a JavaScript codebase to TypeScript incrementally with minimal disruption

Quick Summary30 lines
You are an expert in migrating JavaScript projects to TypeScript for improved type safety, tooling, and maintainability.

## Key Points

1. **Preparation** — audit the codebase, identify entry points and dependency graph.
2. **Bootstrap TypeScript** — add `tsconfig.json` with permissive settings alongside existing JS tooling.
3. **Incremental Rename** — convert files leaf-first (utilities, constants, types) then work inward toward application logic.
4. **Strict Mode Ramp** — enable strict flags one at a time (`noImplicitAny`, `strictNullChecks`, etc.) after a critical mass of files have been converted.
- Convert files leaf-first (no imports from unconverted files) to minimize cascading errors.
- Use `// @ts-expect-error` sparingly as a temporary bridge, and track occurrences with a lint rule.
- Add a CI check that runs `tsc --noEmit` so regressions are caught immediately.
- Adopt `unknown` over `any` wherever possible — it forces explicit narrowing.
- Write new code exclusively in TypeScript from day one of the migration.
- Use `@types/*` packages from DefinitelyTyped for third-party libraries.
- **Big-bang rewrites** — converting everything at once leads to thousands of errors and stalled PRs. Go incremental.
- **Overly loose `any` usage** — sprinkling `any` everywhere compiles clean but defeats the purpose. Track and reduce `any` count over time.

## Quick Example

```bash
npm install --save-dev typescript @types/node
npx tsc --init
```

```bash
# Rename a utility file
mv src/utils/format.js src/utils/format.ts
```
skilldb get migration-patterns-skills/Javascript to TypescriptFull skill: 138 lines
Paste into your CLAUDE.md or agent config

JavaScript to TypeScript — Migration Patterns

You are an expert in migrating JavaScript projects to TypeScript for improved type safety, tooling, and maintainability.

Core Philosophy

Overview

TypeScript adoption does not require a big-bang rewrite. The recommended approach is an incremental migration: rename files one at a time from .js to .ts/.tsx, fix type errors as they surface, and progressively tighten compiler strictness. This keeps the project shippable at every step.

Migration Strategy

  1. Preparation — audit the codebase, identify entry points and dependency graph.
  2. Bootstrap TypeScript — add tsconfig.json with permissive settings alongside existing JS tooling.
  3. Incremental Rename — convert files leaf-first (utilities, constants, types) then work inward toward application logic.
  4. Strict Mode Ramp — enable strict flags one at a time (noImplicitAny, strictNullChecks, etc.) after a critical mass of files have been converted.

Step-by-Step Guide

1. Install TypeScript and initial config

npm install --save-dev typescript @types/node
npx tsc --init

2. Start with a permissive tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "allowJs": true,           // key: lets .js and .ts coexist
    "checkJs": false,
    "outDir": "./dist",
    "strict": false,           // start loose, tighten later
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src"]
}

3. Rename leaf files first

# Rename a utility file
mv src/utils/format.js src/utils/format.ts

Add minimal types to fix errors:

// Before (JS)
export function formatCurrency(amount, locale) {
  return new Intl.NumberFormat(locale, { style: 'currency', currency: 'USD' }).format(amount);
}

// After (TS)
export function formatCurrency(amount: number, locale: string): string {
  return new Intl.NumberFormat(locale, { style: 'currency', currency: 'USD' }).format(amount);
}

4. Use declaration files for third-party JS modules without types

// src/types/legacy-lib.d.ts
declare module 'legacy-lib' {
  export function doSomething(input: string): void;
}

5. Progressively enable strict flags

// Phase 1
"noImplicitAny": true,

// Phase 2
"strictNullChecks": true,

// Phase 3 — full strict
"strict": true

6. Update build tooling

For a project using ESBuild or Webpack, ensure the loader handles .ts files:

// webpack.config.js addition
module.exports = {
  resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'] },
  module: {
    rules: [
      { test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ }
    ]
  }
};

Best Practices

  • Convert files leaf-first (no imports from unconverted files) to minimize cascading errors.
  • Use // @ts-expect-error sparingly as a temporary bridge, and track occurrences with a lint rule.
  • Add a CI check that runs tsc --noEmit so regressions are caught immediately.
  • Adopt unknown over any wherever possible — it forces explicit narrowing.
  • Write new code exclusively in TypeScript from day one of the migration.
  • Use @types/* packages from DefinitelyTyped for third-party libraries.

Common Pitfalls

  • Big-bang rewrites — converting everything at once leads to thousands of errors and stalled PRs. Go incremental.
  • Overly loose any usage — sprinkling any everywhere compiles clean but defeats the purpose. Track and reduce any count over time.
  • Ignoring strictNullChecks — this single flag catches the most real bugs; do not leave it off permanently.
  • Forgetting test files — tests should also be converted; otherwise you lose type coverage on mocks and assertions.
  • Mismatched module systems — ensure tsconfig module settings align with your bundler (CommonJS vs ESM).

Anti-Patterns

Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.

Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.

Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.

Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.

Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.

Install this skill directly: skilldb add migration-patterns-skills

Get CLI access →