TypeScript 97 ๐ท Migrating Between TypeScript Versions
TypeScript releases a new minor version roughly every three months. Most of these releases are additive โ new syntax, new type features, new compiler options. But some releases introduce breaking changes: behavior that compiled and ran correctly in one version produces a different result in the next. The major version boundaries (4.0, 5.0, 6.0) tend to concentrate the breaking changes, but minor versions can also deprecate APIs, tighten inference, or change default settings.
Migrating between TypeScript versions is not the same problem as migrating from JavaScript. The codebase already has types. The risk is that those types were correct under one compiler and are wrong under another. A project that upgrades from TypeScript 4.9 to 5.9 in one step may hit hundreds of new errors โ not because the code became worse, but because the compiler became stricter, the type inference changed, or a library’s type definitions assume a newer compiler feature.
The LFCA and Angular chapters in this series focus on runtime behavior and application structure. This chapter sits at the tooling layer: the compiler, the type definitions, and the CI pipeline that keeps them synchronized.
Key point: TypeScript does not follow semantic versioning in the strictest sense. Minor versions can introduce breaking changes. The TypeScript team publishes release notes for every version, and the “Breaking Changes” section is the one to read before upgrading. The recommended approach is to upgrade one minor version at a time, run tsc --noEmit after each, and fix the errors before moving to the next version .
Why TypeScript version migration matters
TypeScript is a compiler. The version of the compiler determines what types are accepted, what types are inferred, and what errors are reported. A codebase that is type-correct under one version may not be under another. The version of the compiler is not a detail โ it is part of the codebase’s contract.
The inference drift problem. TypeScript’s type inference improves over time. A function that returned a widened type in 4.x may return a narrower type in 5.x. A variable that was implicitly any in one version may be unknown in another. These changes are improvements, but they surface as errors in code that depended on the old behavior. The migration is the process of updating the code to work with the improved inference.
The library types problem. The @types/* packages for libraries are maintained separately from TypeScript itself. A new version of @types/react may require a newer TypeScript version. If the codebase pins TypeScript to an older version, installing the latest @types/react can produce errors that are not the code’s fault. The versions of TypeScript, the type definitions, and the libraries must be compatible.
The compiler option problem. TypeScript occasionally changes the default value of a compiler option or deprecates an option entirely. A project that relies on a default from an older version may find the default changed in a newer version. The tsconfig.json must be updated to match the new defaults or to explicitly set the old behavior.
The tooling problem. ESLint, Jest, Vite, and other tools embed or depend on specific TypeScript versions. A TypeScript upgrade may require upgrading the tooling in lockstep. The typescript-eslint package, for example, declares a supported TypeScript version range. Upgrading TypeScript beyond that range produces warnings and may break lint rules.
The trade-off. Upgrading is not optional forever. Older TypeScript versions stop receiving bug fixes and security patches. Library type definitions eventually require newer compiler features. The choice is when to upgrade, not whether. The recommended cadence is to upgrade one minor version at a time and to keep the codebase within one or two minor versions of the latest release.
a. The Upgrade Process
The upgrade process has five phases: read the release notes, upgrade one version, run the type checker, fix the errors, and repeat.
Read the release notes. Every TypeScript release has a blog post with a “Breaking Changes” section. The TypeScript wiki on GitHub has a page for each version that lists the breaking changes in detail. Reading these before upgrading tells the team what to expect and how many changes are likely.
The typical breaking changes fall into a few categories:
Tighter inference. A type that was inferred as any may now be inferred as unknown. A union that was inferred as string may now be inferred as string | undefined. The fix is to add explicit annotations where the inferred type is not what the code needs.
Deprecated APIs. TypeScript’s own compiler APIs (ts.createProgram, ts.SourceFile, etc.) occasionally deprecate functions. This affects tools that build on TypeScript, not application code. For application code, the deprecated items are usually compiler flags or syntax that was never fully supported.
Changed defaults. The default value of a compiler option changes. The most significant example is the types field in tsconfig.json, which began defaulting to an empty array in TypeScript 6.0 . This means @types/* packages are no longer automatically included; they must be listed explicitly in the types field.
Stricter checks. A rule that was previously a warning becomes an error, or a new check is added that catches code that was previously accepted. The strict option’s bundle grows over time. Code that compiled with strict: true in 4.x may have new errors in 5.x.
Upgrade one version. The npm install typescript@5.4 command installs a specific version. The upgrade should be one minor version at a time. Upgrading from 4.9 to 5.9 in one step accumulates the breaking changes of ten releases. Upgrading from 5.3 to 5.4 isolates the changes of one release.
Run the type checker. The npx tsc --noEmit command runs the compiler in check-only mode. The output is the list of errors introduced by the new version. The team fixes these errors before the next upgrade.
Fix the errors. The errors fall into a small number of categories. Each has a standard fix:
- Implicit
any: add a type annotation or useunknown. - Narrowed type: add a type guard or a type assertion.
- Changed API: update the call to the new API.
- Deprecated option: remove the option or replace it with the new one.
- Missing
typesentry: add the@types/*package to thetypesarray.
Repeat. After the errors are fixed and the new version compiles cleanly, the next minor version can be installed. The whole process can take hours or days, depending on the size of the codebase and the number of breaking changes.
b. Common Breaking Changes by Version
The breaking changes that affect application code most often are the ones that tighten inference and change defaults. A few examples from recent versions illustrate the pattern.
TypeScript 4.x โ 5.0. The moduleResolution: "bundler" option was introduced. The verbatimModuleSyntax option was added. Type parameter defaults changed in some cases. The --target ES3 support was deprecated.
TypeScript 5.x โ 5.5. Inferred type predicates were added, which improved type narrowing in some functions but could change the inferred type of others. The isolatedDeclarations option was introduced. Control flow narrowing was tightened.
TypeScript 5.x โ 5.9. The import defer syntax was introduced. The --module node20 option was added. A new check for Uint8Array generic usage was added.
TypeScript 5.x โ 6.0. The types field in tsconfig.json began defaulting to an empty array. This is the most impactful change for projects that relied on automatic inclusion of @types/* packages. Every @types package that the project uses must now be listed explicitly. The moduleResolution: "node" option was deprecated in favor of node10. The esModuleInterop and allowSyntheticDefaultImports behavior was tightened.
The pattern across these versions is consistent: TypeScript gets stricter over time. The code that compiled cleanly under the old version may have new errors under the new version. The errors are not bugs โ they are opportunities to make the code more type-safe. The migration is the process of taking advantage of those opportunities.
For each version, the TypeScript wiki has a “Breaking Changes” page. The page for the current version is the first thing to read before upgrading. It lists every change that could break existing code, with examples and the recommended fix.
c. The Tooling Ecosystem
TypeScript does not exist in isolation. The compiler is used by the editor, the linter, the test runner, the bundler, and the type definition packages. Upgrading TypeScript means checking that all of these are compatible with the new version.
The editor. VS Code bundles its own TypeScript version, but it can be configured to use the workspace version. The typescript.tsdk setting in .vscode/settings.json points the editor at the node_modules/typescript/lib directory. This ensures that the editor shows the same errors as the CI pipeline.
The linter. typescript-eslint declares a supported TypeScript version range. Upgrading TypeScript beyond that range produces warnings and may break lint rules. The typescript-eslint release notes document the supported range for each version. The linter is typically upgraded after TypeScript, once the new version is in use.
The test runner. Jest and Vitest use TypeScript through ts-jest, @swc/jest, or Vite’s esbuild. The test runner does not type-check by default โ it only transpiles. The type checking is done separately by tsc --noEmit. The test runner’s TypeScript support is usually version-agnostic, but the transpiler’s supported syntax may lag behind the compiler.
The bundler. Vite and esbuild transpile TypeScript without type-checking. Their TypeScript support is version-agnostic in most cases, but new syntax may not be supported until the transpiler is updated. The verbatimModuleSyntax option, for example, requires the bundler to handle type-only imports correctly.
The type definitions. The @types/* packages are published by the DefinitelyTyped community. They declare their supported TypeScript version in package.json. Installing the latest @types/react may require a newer TypeScript version than the project uses. The npm install command warns about the mismatch, but does not prevent it.
The recommended upgrade order is:
- Upgrade TypeScript.
- Run
tsc --noEmit. - Fix the errors.
- Upgrade the type definitions.
- Upgrade
typescript-eslint. - Upgrade the test runner and the bundler if needed.
Each step is a separate commit. Each commit is a checkpoint that can be reverted if something breaks.
Complete Example Session
This session upgrades a project from TypeScript 5.8 to 5.9 and then to 6.0, fixing the errors introduced at each step.
// ============================================
// PART 1: THE STARTING POINT
// ============================================
// package.json
{
"devDependencies": {
"typescript": "5.8.0",
"typescript-eslint": "^8.0.0",
"@types/node": "^20.0.0"
}
}
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
// The project compiles with zero errors.
// ============================================
// PART 2: UPGRADE TO TYPESCRIPT 5.9
// ============================================
// npm install --save-dev typescript@5.9
// Run the type checker
npx tsc --noEmit
// Error: Cannot find module 'node:fs' or its corresponding type declarations.
// Cause: TypeScript 5.9 changed the module resolution for node: prefixes.
// Fix: Update @types/node to a version that supports the new behavior.
// npm install --save-dev @types/node@22
// Run again
npx tsc --noEmit
// Error: Type 'Uint8Array<ArrayBufferLike>' is not assignable to
// type 'Uint8Array<ArrayBuffer>'.
// Cause: TypeScript 5.9 added a generic parameter to Uint8Array.
// Fix: Update the code to use the generic form.
// Before
function readBytes(buffer: Uint8Array): number {
return buffer.length;
}
// After
function readBytes(buffer: Uint8Array<ArrayBufferLike>): number {
return buffer.length;
}
// Run again
npx tsc --noEmit
// No errors. The upgrade to 5.9 is complete.
// ============================================
// PART 3: UPGRADE TO TYPESCRIPT 6.0
// ============================================
// npm install --save-dev typescript@6.0
// Run the type checker
npx tsc --noEmit
// Error: Cannot find name 'describe'. Cannot find name 'it'. Cannot find name 'expect'.
// Cause: TypeScript 6.0 changed the default for the `types` field.
// Previously, all @types/* packages were automatically included.
// Now the default is an empty array.
// Fix: Add the types explicitly to tsconfig.json.
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"types": ["node", "vitest/globals"]
},
"include": ["src/**/*"]
}
// Run again
npx tsc --noEmit
// Error: Option 'moduleResolution: node' is deprecated. Use 'node10' instead.
// Cause: TypeScript 6.0 deprecated the "node" value.
// Fix: Replace "node" with "node10" or "bundler" depending on the project.
// tsconfig.json (already using "bundler" โ no change needed for this project)
// Run again
npx tsc --noEmit
// No errors. The upgrade to 6.0 is complete.
// ============================================
// PART 4: UPGRADE THE LINTER
// ============================================
// npm install --save-dev typescript-eslint@latest
// Check the compatibility
npx eslint .
// If the linter reports a version mismatch, check the typescript-eslint
// release notes for the supported TypeScript version range.
// The linter is usually upgraded after TypeScript.
// ============================================
// PART 5: UPDATE THE EDITOR
// ============================================
// .vscode/settings.json
{
"typescript.tsdk": "node_modules/typescript/lib"
}
// This tells VS Code to use the workspace TypeScript version
// instead of the version bundled with the editor.
// ============================================
// PART 6: THE CI PIPELINE
// ============================================
// .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm run typecheck
- run: npm run lint
- run: npm run test
// The typecheck step runs tsc --noEmit with the upgraded version.
// ============================================
// PART 7: THE DEPRECATED OPTIONS CHECK
// ============================================
// Run tsc with the --showConfig flag to see the resolved config
npx tsc --showConfig
// Look for deprecation warnings in the output.
// Remove or replace any deprecated options.
// ============================================
// PART 8: THE VERSION RANGE IN package.json
// ============================================
// package.json
{
"devDependencies": {
"typescript": "~5.9.0"
}
}
// The ~ (tilde) allows patch updates within the minor version.
// The ^ (caret) allows minor updates within the major version.
// For TypeScript, the recommended range is ~ to control minor upgrades.
// ============================================
// PART 9: THE ROLLBACK PLAN
// ============================================
// Each upgrade is a separate commit.
// If an upgrade introduces errors that cannot be resolved quickly,
// the commit can be reverted.
// git revert <commit-hash>
// The codebase returns to the previous TypeScript version.
// The upgrade can be retried later with more time.
// ============================================
// PART 10: THE UPGRADE CHECKLIST
// ============================================
// 1. Read the release notes for the target version
// 2. Install the new TypeScript version
// 3. Run tsc --noEmit
// 4. Fix the errors
// 5. Upgrade @types/* packages if needed
// 6. Upgrade typescript-eslint
// 7. Update the editor's TypeScript version
// 8. Verify the CI pipeline
// 9. Check for deprecated options
// 10. Commit the upgrade as a separate change
The ten parts cover the starting point, the upgrade to 5.9, the upgrade to 6.0, the linter upgrade, the editor configuration, the CI pipeline, the deprecated options check, the version range, the rollback plan, and the upgrade checklist.
Quick Reference
The Upgrade Process
| Step | Command | Purpose |
|---|---|---|
| Install | npm install -D typescript@5.9 | Upgrade the compiler |
| Check | npx tsc --noEmit | Find new errors |
| Fix | Edit source files | Resolve errors |
| Verify | npx tsc --noEmit | Confirm zero errors |
| Commit | git commit -m "Upgrade to TS 5.9" | Checkpoint |
The Common Breaking Changes
| Category | Example | Fix |
|---|---|---|
| Tighter inference | any โ unknown | Add type annotations |
| Changed defaults | types defaults to [] | List types explicitly |
| New checks | Uint8Array generic | Update the type |
| Deprecated options | moduleResolution: node | Use node10 or bundler |
| Deprecated APIs | Compiler APIs | Update to the new API |
The Tooling Compatibility
| Tool | Checks TypeScript Version | Upgrade Order |
|---|---|---|
typescript-eslint | Yes | After TypeScript |
@types/* | Yes | After TypeScript |
| VS Code | Configurable | Same as workspace |
| Vitest / Jest | No | Independently |
| Vite / esbuild | No | Independently |
The tsconfig Changes by Version
| Version | Change | Action |
|---|---|---|
| 5.0 | moduleResolution: bundler added | Optional |
| 5.9 | Uint8Array generic | Update types |
| 6.0 | types defaults to [] | List types explicitly |
| 6.0 | moduleResolution: node deprecated | Use node10 or bundler |
Best Practices
โ Do This:
# Upgrade one minor version at a time
npm install --save-dev typescript@5.9 # โ
# Run the type checker after every upgrade
npx tsc --noEmit # โ
# Read the release notes before upgrading
# The "Breaking Changes" section is the one to read. # โ
// Pin the TypeScript version with a tilde
{ "typescript": "~5.9.0" } // โ
// Set the editor to use the workspace TypeScript
{ "typescript.tsdk": "node_modules/typescript/lib" } // โ
โ Don’t Do This:
# Don't upgrade across multiple major versions at once
npm install --save-dev typescript@6.0 # from 4.x # โ
// Don't use a caret range for TypeScript
{ "typescript": "^5.0.0" } // may pull 5.9 unexpectedly // โ
# Don't skip the type check after upgrading
npm install typescript@5.9 && git commit # โ
// Don't forget to update the types field for 6.0
{ "compilerOptions": { "strict": true } } // no "types" // โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Hundreds of errors | Upgraded too far in one step | Upgrade one minor version at a time |
| Linter breaks | typescript-eslint version mismatch | Upgrade the linter after TypeScript |
| Editor shows different errors | Editor uses bundled TypeScript | Set typescript.tsdk |
@types mismatch | Installed latest @types/* | Match to the TypeScript version |
| CI fails | CI uses an older TypeScript | Update package.json and lock file |
| Missing globals in 6.0 | types defaults to [] | Add "types": ["node"] |
Real-World Examples
1. Upgrade One Version
npm install --save-dev typescript@5.9
npx tsc --noEmit
2. Fix a Narrowed Type
// Before: implicit any
function parse(data) { return data.value; }
// After: explicit unknown
function parse(data: unknown): string {
if (typeof data === 'object' && data !== null && 'value' in data) {
return String((data as { value: unknown }).value);
}
throw new Error('Invalid data');
}
3. Update the types Field
{ "compilerOptions": { "types": ["node", "vitest/globals"] } }
4. Deprecated moduleResolution
// Before
{ "moduleResolution": "node" }
// After
{ "moduleResolution": "bundler" }
5. Update @types/node
npm install --save-dev @types/node@22
6. Update typescript-eslint
npm install --save-dev typescript-eslint@latest
7. Editor Configuration
{ "typescript.tsdk": "node_modules/typescript/lib" }
8. CI Step
- run: npm run typecheck
9. Pin the Version
{ "typescript": "~5.9.0" }
10. Rollback
git revert <commit-hash>
Visual
The Upgrade Flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ UPGRADE FLOW โ
โ โ
โ Read release notes โ
โ โ โ
โ โผ โ
โ npm install typescript@5.9 โ
โ โ โ
โ โผ โ
โ npx tsc --noEmit โ
โ โ โ
โ โโ 0 errors โ commit and repeat โ
โ โ โ
โ โโ errors โ fix โ
โ โ โ
โ โผ โ
โ npx tsc --noEmit โ 0 errors โ
โ โ โ
โ โผ โ
โ commit and repeat โ
โ โ
โ Each minor version is a separate checkpoint.โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Breaking Change Categories
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ BREAKING CHANGE CATEGORIES โ
โ โ
โ Tighter inference: โ
โ any โ unknown โ
โ string โ string | undefined โ
โ Fix: add annotations โ
โ โ
โ Changed defaults: โ
โ types now defaults to [] โ
โ Fix: list types explicitly โ
โ โ
โ Deprecated options: โ
โ moduleResolution: node โ node10 โ
โ Fix: use the new value โ
โ โ
โ Stricter checks: โ
โ new rules catch old code โ
โ Fix: update the code โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Tooling Upgrade Order
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ TOOLING UPGRADE ORDER โ
โ โ
โ 1. TypeScript โ
โ โ โ
โ โผ โ
โ 2. @types/* packages โ
โ โ โ
โ โผ โ
โ 3. typescript-eslint โ
โ โ โ
โ โผ โ
โ 4. Editor (tsdk setting) โ
โ โ โ
โ โผ โ
โ 5. Test runner / bundler (if needed) โ
โ โ
โ Each step is a separate commit. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The CI Pipeline
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ CI PIPELINE โ
โ โ
โ Push / PR โ
โ โ โ
โ โผ โ
โ npm ci โ
โ โ โ
โ โผ โ
โ tsc --noEmit โโ> type errors? โ
โ โ โ
โ โโ YES โ block merge โ
โ โโ NO โ continue โ
โ โ โ
โ โผ โ
โ eslint . โโ> lint errors? โ
โ โ โ
โ โโ YES โ block merge โ
โ โโ NO โ continue โ
โ โ โ
โ โผ โ
โ vitest run โโ> test failures? โ
โ โ โ
โ โโ YES โ block merge โ
โ โโ NO โ merge allowed โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| Upgrade cadence | One minor version at a time |
| Release notes | The “Breaking Changes” section |
| Type checker | npx tsc --noEmit after each upgrade |
| Common change | Tighter inference (any โ unknown) |
| Common change | types defaults to [] in 6.0 |
| Common change | moduleResolution: node deprecated in 6.0 |
| Tooling order | TypeScript โ @types โ typescript-eslint |
| Editor config | typescript.tsdk in VS Code settings |
| Version pin | ~5.9.0 (tilde, not caret) |
| Rollback | git revert the upgrade commit |
Key takeaways:
- TypeScript minor versions can introduce breaking changes. The compiler gets stricter over time. Code that compiled cleanly under one version may have new errors under the next. The release notes’ “Breaking Changes” section is the source of truth for what changed.
- Upgrade one minor version at a time. Jumping from 4.9 to 6.0 accumulates the breaking changes of a dozen releases. Upgrading one version at a time isolates the changes and makes the errors manageable.
- Run
tsc --noEmitafter every upgrade. The type checker is the tool that reports the errors introduced by the new version. The fix for each error is a small change to the code or the configuration. - The common breaking changes are inference tightening, default changes, and deprecated options. TypeScript 6.0 changed the default for the
typesfield to an empty array, which requires every@typespackage to be listed explicitly. TypeScript 6.0 also deprecatedmoduleResolution: nodein favor ofnode10orbundler. - Upgrade the tooling after TypeScript. The
@types/*packages andtypescript-eslintdeclare their supported TypeScript version ranges. Upgrading them before TypeScript produces warnings. The recommended order is TypeScript, then@types, then the linter, then the editor, then the test runner and bundler. - Set the editor to use the workspace TypeScript. The
typescript.tsdksetting in.vscode/settings.jsonensures that the editor shows the same errors as the CI pipeline. Without it, the editor may use a bundled version of TypeScript that is different from the project’s. - Pin the TypeScript version with a tilde. The
~5.9.0range allows patch updates within the minor version. The^5.9.0range allows minor updates that may introduce breaking changes. TypeScript is not a library where caret ranges are appropriate.
Remember: Migrating between TypeScript versions is not a single event. It is a process that happens every few months when a new version is released. The process is: read the release notes, upgrade one version, run the type checker, fix the errors, upgrade the tooling, and commit. Each upgrade is a checkpoint that can be reverted if something breaks. The compiler gets stricter over time, and the codebase gets safer with it. The migration is the price of the improvements.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!