Home/Learn/JavaScript & TypeScript/Migrating and Living With TypeScript

Migrating and Living With TypeScript

Advanced
TypeScript

How to add TypeScript to an existing codebase without a rewrite, and how to keep any from spreading once it is in.

Overview

You will more often join a partially typed codebase than start a clean one, so the migration skills matter. TypeScript is designed for incremental adoption: allowJs lets the two coexist, checkJs applies checks to plain JavaScript, and strictness can be raised directory by directory. The discipline that decides whether it works is what you do with `any` — it is contagious, spreading through every value it touches, and a codebase that reaches for it under time pressure ends up with the cost of TypeScript and none of the benefit.

Incremental Adoption

A migration order that keeps the build green throughout.

Loose config, leaf-first, then ratchet
// 1. Add TypeScript with the loosest possible config
{ "compilerOptions": { "allowJs": true, "strict": false, "noEmit": true } }

// 2. Convert leaf-first: utilities and types before components.
//    A typed module makes everything importing it better; a typed
//    component with untyped dependencies fights inference.

// 3. Turn on strict flags one at a time, fixing as you go
"noImplicitAny": true        // usually the largest single batch
"strictNullChecks": true     // usually the most valuable

// 4. Check JS files without renaming them
// @ts-check at the top of a .js file, with JSDoc types:
/** @param {string} slug @returns {Promise<Problem>} */
async function getProblem(slug) { ... }

// 5. Ratchet: never let the error count go up.
//    tsc --noEmit in CI, and each PR must not add errors.

Containing any

any spreads. These are the tools for keeping it at the edges.

unknown at the edge, @ts-expect-error over @ts-ignore
// any infects everything downstream
const data: any = await res.json()
const name = data.user.name        // any
const upper = name.toUpperCase()   // any — no checking anywhere below

// unknown stops the spread at the source
const data: unknown = await res.json()
// forced to narrow before any use

// Suppressions, in order of preference
// @ts-expect-error — errors if the line STOPS failing, so it self-cleans
// @ts-ignore       — silent forever, including after the bug is fixed

// Find where any lives
npx type-coverage --detail        // percentage of typed expressions
// and forbid new ones:
// eslint: @typescript-eslint/no-explicit-any, no-unsafe-assignment

// Third-party modules with no types
declare module 'untyped-lib' {
  export function doThing(input: string): Promise<void>
}
// Better than 'any' — one small honest declaration instead of a hole.

Build Setup

Which tool does what. Bundlers strip types; only tsc actually checks them.

The bundler strips; tsc --noEmit checks
// esbuild, swc, Vite, Next: transpile TypeScript by DELETING types.
// They are fast because they never type-check. Which means:
//   a broken type does not fail your dev server or your build.

// So run the checker separately, and in CI
"scripts": {
  "typecheck": "tsc --noEmit",
  "build": "next build",
  "ci": "npm run typecheck && npm run lint && npm test"
}

// Because types are stripped per-file, these need explicit syntax:
import type { User } from './types'        // erased for certain
export type { User }
"isolatedModules": true                    // enforces it

// Speed, for large repos
"incremental": true                        // caches in .tsbuildinfo
// project references to split the graph into checked units

Key Points to Remember

  • 1Adopt incrementally: allowJs with strict off, convert leaf modules first, then enable strict flags one at a time
  • 2@ts-check with JSDoc types checks a .js file without renaming it
  • 3any spreads to every value it touches; unknown at the boundary stops the spread
  • 4@ts-expect-error is preferable to @ts-ignore because it errors once the underlying problem is fixed
  • 5Bundlers strip types without checking them — tsc --noEmit in CI is what actually enforces the types

Interview Questions

Sign in to ask Aria
1

How would you introduce TypeScript into a large existing JavaScript codebase?

Hard
2

What is the difference between @ts-ignore and @ts-expect-error?

Medium
3

Why can a project build successfully while containing type errors?

Medium

Ask Aria about Migrating and Living With TypeScript

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…