My Take on TypeScript Versioning: Love, Fear, Repeat

Hi, I’m Kayla. I write apps for work and for fun. TypeScript is my daily buddy. Versioning, though? It can be sweet. It can also eat a whole Friday. Been there. I’ve talked about that exact love-fear-repeat cycle in more depth over in Love, Fear, Repeat, but here’s the quick version.

Let me explain what helps, what hurts, and what I actually do when a new version drops.

The quick picture

  • I pin my TypeScript version. No loose carets.
  • I upgrade on a branch, with tests ready.
  • I match my editor to my project’s TS. Always.
  • I keep an eye on ESLint, ts-node, Jest, and build tools, since they can lag.

Simple rules. They save me.


Pin it, or regret it

Here’s how my package.json looks on a real app I ship:

{
  "devDependencies": {
    "typescript": "5.5.4"
  }
}

I set npm to use tilde tags, so patch bumps are fine, but minor bumps don’t sneak in.

npm config set save-prefix="~"

Why? One time I had "typescript": "^5.1.0". Next install jumped to 5.2. A new check hit my types. Guess what broke at 3 pm? Yeah.

If you’re curious about everything that shipped in this release family, the official release notes lay it all out in detail.


Real story: 4.9 to 5.x and the “oh no” test run

I loved 4.9. The new satisfies operator? Chef’s kiss. I used it a lot:

const theme = {
  primary: "#1e90ff",
  spacing: 8,
} satisfies Record<string, string | number>;

Then I bumped to 5.0 on a feature branch. New goodies showed up (decorators, const type parameters). But my tests failed on CI. ts-jest didn’t match yet. I got type errors plus one weird compile time hit.

Fix was boring, but it worked:

  • Update ts-jest and jest.
  • Clear caches.
  • Run tsc --noEmit.
npm i -D ts-jest@latest jest@latest
npx jest --clearCache
npx tsc --noEmit

I also toggled a new flag that my team liked later:

{
  "compilerOptions": {
    "verbatimModuleSyntax": true
  }
}

It made imports cleaner, but also stricter. Two files needed changes. Annoying, but tidy in the end.


VS Code mismatch: the sneaky one

This bit bit me more than once. My project used TS 5.5. VS Code used its own 5.0 copy. The editor screamed. The build didn’t. I thought I was losing it. It reminded me of the headaches I ran into while renaming a TypeScript field while keeping your JSDoc; tooling looks small until it bites.

The fix:

  • In VS Code, hit the TypeScript version in the status bar.
  • Pick “Use Workspace Version.”

Feels tiny. Saves hours. I now set this on every repo.


Monorepo chaos, meet pnpm overrides

On a big repo, some packages pulled in their own TypeScript. Oh no. Different versions. Different errors.

I forced one version with pnpm:

{
  "pnpm": {
    "overrides": {
      "typescript": "5.5.4"
    }
  }
}

After that, all packages lined up. CI got quiet. My coffee got warm again.

Yarn folks, I’ve used this too:

{
  "resolutions": {
    "typescript": "5.5.4"
  }
}

It’s not fancy. It’s stable.


tsconfig flags that move with versions

Some flags feel small but change your day.

Here’s a baseline I use for web apps:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "skipLibCheck": true
  }
}
  • exactOptionalPropertyTypes (from the 4.x era) is strict. It found bugs in my API layer. But it also made a few union types noisy. I keep it on now. If you want to see how the rule interacts with optional parameters, I broke it down step by step.
  • moduleResolution: "NodeNext" helped with ESM packages. It also forced me to fix imports that had loose extensions. Small pain, better build.

While we’re talking strictness, don’t forget about the notorious exclamation mark operator—knowing when to drop it keeps my diffs small.

And yeah, skipLibCheck: true is my calm button. It speeds builds, and I sleep fine.


Library work: serving two crowds with typesVersions

I ship a small date helper lib for my team. Some apps stayed on 4.7. Others ran 5.x. I split types like this:

{
  "types": "dist/index.d.ts",
  "typesVersions": {
    "<4.8": { "*": ["types/ts4/*"] },
    ">=4.8": { "*": ["types/ts5/*"] }
  }
}

It took one extra build step. But folks stopped pinging me with “types broke” messages. Worth it.

For more depth, the TypeScript docs have a succinct section on how to version declaration files that’s great reference material when you’re publishing packages.


Tooling that follows TS (or doesn’t)

This part matters more than people think.

  • typescript-eslint: I update it with TS. If ESLint rules lag, my CI fails. Not fun.
  • ts-node vs tsx: tsx has been smoother for me with ESM and newer TS. Less config, fewer sighs.
  • Vite, tsup, and SWC: Fast builds, but watch their TS peer ranges.
  • @types/* packages: Sometimes they need a bump when TS gets picky.

I now do upgrades in this order:

  1. TypeScript
  2. ESLint + typescript-eslint
  3. Test runner (Jest or Vitest)
  4. Build tool

Then I re-run the editor using the workspace TS. Then I breathe.


My upgrade playbook (the one I actually use)

If you want a step-by-step look at automating this flow in CI, check out this walkthrough on Improving Code — it pairs nicely with the checklist below.

  • Make a branch: feat/ts-5-6-upgrade
  • Update TS:
npm i -D typescript@5.6.3
npx tsc --version
  • Make sure VS Code uses workspace TS.
  • Run:
npx tsc --noEmit
npm run lint
npm test
  • Update typescript-eslint and friends if they complain.
  • Push. Open PR. Ask one teammate to try on their machine.

That’s it. Nothing fancy. Very safe.


Favorite feature hits by version (quick notes)

  • 3.7: Optional chaining and nullish coalescing. I still smile.
  • 4.1: Template literal types. I used them for route maps.
  • 4.9: satisfies. Great for config objects.
  • 5.0: Decorators and const type parameters. I used decorators in a Nest-like service.
  • 5.2: using declarations got support. I tested them in a file tool. Clean release paths.

Oh, and if you’ve ever been knee-deep in generics and suddenly realized you forgot a type argument, here’s what actually happens under the hood: forgetting a type parameter. Side note: 5.x also unlocked some neat ergonomics like using named arguments in constructors—great for those classes with mile-long parameter lists.

I don’t chase every flag. I add what I need. Then I ship.


The good, the bad, the “why is CI red?”

What I love

  • Clear errors. Strong types. Fewer runtime “oops.”
  • Helpful flags that grow with the language.
  • Better editor smarts with each bump.

What bugs me

  • Tools lag. ESLint and test runners sometimes need time.
  • Editor mismatch. It’s too