Right, so you’ve got a large codebase. It’s a beautiful, intricate, snowflake of legacy logic that somehow still makes the company money. And now you want to upgrade TypeScript. Excellent. This is where the real engineering begins, and by “engineering,” I mean a careful blend of archaeology, diplomacy, and strategic flag-planting.

The biggest mistake you can make is running npm update typescript on a Friday afternoon and hoping for the best. Hope is not a strategy; it’s a prelude to a weekend of regret. TypeScript’s core mission is to find new and exciting ways to tell you your code was already broken, you just didn’t know it yet. An upgrade is it turning up the sensitivity on its metal detector.

Your New Best Friend: The TypeScript Upgrade Tool

Forget manually sifting through thousands of errors. The TypeScript team provides a lifesaver: the @typescript/eslint-plugin-upgrade tool. This isn’t just a nice-to-have; for a large project, it’s non-negotiable.

First, you’ll target a specific version. Let’s say you’re moving from 4.8 to 5.0.

# Get the tool
npm install @typescript/eslint-plugin-typescript@next --save-dev

# Run it for your target version
npx @typescript/eslint-plugin-upgrade typescript-version-5.0

This command doesn’t change a single line of code. Instead, it runs an ESLint plugin over your codebase that identifies exactly where the new, stricter version of the compiler will yell at you. It gives you a list of errors and their specific locations before you even change the tsconfig.json. This is the equivalent of getting the enemy’s battle plans delivered to your doorstep. Use it.

Taming the tsconfig.json Beast

Your tsconfig.json is your control panel. When upgrading, you have two primary levers to pull: target and lib.

The target setting dictates what JavaScript version your code is compiled down to (ES2015, ES2022, etc.). This is separate from the TypeScript version itself. You can be on TypeScript 5.3 and still output ES2015 code for browser compatibility. The upgrade pressure usually comes from the lib setting.

The lib setting tells TypeScript what ambient types to assume are available (e.g., Promise, Map, document.getElementById). When you upgrade TypeScript, it introduces a new lib definition file by default, reflecting the latest JavaScript features.

This can cause immediate havoc. Suddenly, TypeScript might assume Array.prototype.findLast exists everywhere, but if your target is still set to an older ES version that doesn’t include it, your compiled code will explode at runtime. The solution? Be explicit.

{
  "compilerOptions": {
    "target": "ES2020", // Your compilation target
    "lib": [
      "ES2020",
      "DOM",
      "DOM.Iterable"
    ] // Explicitly define your runtime environment
  }
}
**Best Practice:** Never let TypeScript implicitly choose your `lib`. Explicitly declare the set of APIs you know your target runtime (e.g., Node.js 18, Chrome 102) will support. This decouples your TypeScript upgrade from your runtime compatibility.

### The `strict` Family of Options: A Gradual Unlocking

TypeScript's `strict` mode is actually a bundle of individual flags. When you upgrade, new strictness flags might be introduced or existing ones might get smarter. The key to a manageable upgrade is to treat these flags individually.

Don't just set `"strict": true` and call it a day. Break it down. Comment out your `"strict": true` and replace it with the explicit set of flags it enables. Now you have a kill switch for each individual type of strictness.

```json
{
  "compilerOptions": {
    // "strict": true,
    "noImplicitAny": true,
    "noImplicitThis": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitReturns": true,
    "exactOptionalPropertyTypes": true, // This one's a new troublemaker
    "useUnknownInCatchVariables": true  // Another fun addition
  }
}
See those last two? They might have been added after your last upgrade. Now, if the upgrade causes a tsunami of errors, you can temporarily disable `exactOptionalPropertyTypes`, fix the other thousand errors, and then come back to tackle that specific new rule later. It turns a binary, all-or-nothing switch into a series of manageable feature flags. This is how you avoid mutiny from your team.

### Dealing with Dependency Hell

Your code is only half the battle. Your `node_modules` is the other half. That one library you depend on that hasn't been updated since the TypeScript 2.0 era will now be a source of red squiggles.

TypeScript has a solution for this, too: `skipLibCheck`. It's basically the compiler saying, "I will look away while you do whatever it is you're doing in there." For an upgrade period, setting `"skipLibCheck": true` is a pragmatic and often necessary choice to isolate *your* errors from your dependencies' errors. You can always set it back to `false` later and deal with the type issues in third-party libs one by one.

The upgrade path isn't a single command; it's a process. Target a specific version, use the tools to pre-scout the problems, adjust your config with surgical precision, and isolate your dependencies. Do it right, and you get all the new goodies—like satisfies and using—without the migraine.