TypeScript 5.0 through 5.9

This document covers breaking changes introduced across TypeScript 5.0 through 5.9 that are likely to surface during a routine upgrade. These accumulate — upgrading from 5.0 to 6.0 means encountering all of them.

All enums are now union enums

Every enum member now gets its own unique type. Previously only enums with literal initializers were treated as union enums. This can break code that assigns computed/non-literal enum values across enum boundaries.

1
2
3
4
enum E { A = Math.random() }
declare let e: E;
declare let n: number;
e = n; // Error: Type 'number' is not assignable to type 'E'

Fix: use an explicit cast e = n as E or restructure the code.

--module and --moduleResolution must agree on Node.js settings

When using node16 or nodenext for either --module or --moduleResolution, the other must also use a Node.js-related setting. Mismatches like --module esnext --moduleResolution node16 are now rejected.

Fix: use --module nodenext alone, or use --module esnext --moduleResolution bundler.

Consistent export checking for merged symbols in ambient contexts

When two declarations merge in ambient contexts (declaration files, declare module blocks), they must now agree on whether they are both exported.

1
2
3
4
5
6
7
declare module 'my-lib' {
    export function foo(): void;
    export {};
    namespace foo { // Error: must also be exported
        export function bar(): void;
    }
}

Fix: add export to both, or remove export {} so all declarations are implicitly exported.

super property accesses on instance fields now error

TypeScript now errors when super.x refers to a class field (as opposed to a prototype method), since this is undefined at runtime.

1
2
3
4
5
6
7
8
class Base {
    myMethod = () => {};
}
class Derived extends Base {
    call() {
        super.myMethod(); // Error: 'super' cannot access instance fields
    }
}

This is always a usercode bug.

More accurate conditional type constraints

A conditional type like T extends Foo ? A : B where T is generic no longer eagerly resolves to the false branch when T’s constraint doesn’t extend Foo. It produces a union instead.

1
2
3
4
type IsArray<T> = T extends any[] ? true : false;
function foo<U extends object>(x: IsArray<U>) {
    let second: false = x; // Error (previously allowed)
}

Fix: don’t assume the conditional type resolves to a single branch when the type parameter is generic. This will likely be a complicated fix.

More aggressive reduction of intersections with type variables and primitives

Intersections of type variables with primitives are now reduced more aggressively. T & number becomes never if T’s constraint is known to be incompatible with number.

1
2
3
function foo<T extends "abc" | "def">(x: T, num: number) {
    let b = intersect(x, num); // Was 'T & number', now is 'never'
}

The correct fix is context-dependent.

Errors when type-only imports conflict with local values (under isolatedModules)

A type-only import that has the same name as a local value declaration is now an error:

1
2
import { Something } from "./some/path";
let Something = 123; // Error under isolatedModules

Fix: add the type modifier to the import, or rename the local variable.

Enum members cannot be named Infinity, -Infinity, or NaN

1
2
3
4
enum E {
    Infinity = 0,  // Error
    NaN = 2,       // Error
}

This is always a usercode bug.

Inferred type predicates from filter-like callbacks

TypeScript now infers type predicates for filter-like functions. Code that relied on the wider pre-filter type may break:

1
2
3
4
// Previously: nums was (number | null)[]
// Now: nums is number[]
const nums = [1, 2, 3, null].filter(x => x !== null);
nums.push(null); // Error in 5.5+

Fix: add an explicit type annotation if the wider type was intended instead.

Regular expression syntax checking

Invalid patterns, non-existent backreferences, and features unavailable at the configured target now produce errors:

1
2
3
let re = /@typedef \{import\((.+)\)\} \3/u;
//                                     ~
// Error: backreference \3 refers to group that doesn't exist

Fix: correct the regex, if the intended meaning was clear, or upgrade target to the necessary ES version that supports the needed regex feature. Otherwise, treat as a usercode bug.

Disallowed nullish and truthy checks

Expressions that are syntactically always truthy or always nullish now error:

1
2
3
if (/0x[0-9a-f]/) { } // Forgot to call .test(s) ?
if (x => 0) { }       // Probably meant to write <=
value < options.max ?? 100; // Missing parens on right side

If the intended meaning was obvious (usually missing parens), fix it, otherwise flag as a usercode bug.

Respecting file extensions and package.json inside node_modules

TypeScript now reads .mjs/.cjs extensions and package.json "type" fields inside node_modules in all module modes, not only under node16/nodenext. Previously-valid default imports from ESM packages may now error:

1
2
// May now fail if "dep" is ESM and this is a default import in a CJS context
import dep from "dep";

Fix: use namespace import import * as dep from "dep" or switch module settings.

Correct override checks on computed properties

Computed properties marked override now properly check for the existence of a base class member, and noImplicitOverride now catches missing override on computed properties that shadow base members.

Checks for never-initialized variables

Variables that are never initialized (not just possibly uninitialized) now error when accessed from nested functions:

1
2
3
4
5
6
function foo() {
    let result: number;
    function printResult() {
        console.log(result); // Error: used before assigned
    }
}

Unless the fix is obvious, treat this as a usercode bug.

TypedArrays are now generic over ArrayBufferLike

All TypedArray types (e.g. Uint8Array) now have a type parameter for the buffer type. This can break code using Node.js Buffer:

error TS2322: Type 'Buffer' is not assignable to type 'Uint8Array<ArrayBufferLike>'.

Fix: update @types/node to the latest version.

Validated JSON imports in --module nodenext

JSON file imports now require an import attribute with type: "json":

1
2
- import myConfig from "./myConfig.json";
+ import myConfig from "./myConfig.json" with { type: "json" };

Also, named exports from JSON are no longer available; use the default import instead.

Granular checks for branches in return expressions

Each branch of a conditional expression in a return statement is now independently checked against the return type:

1
2
3
4
5
function getUrl(s: string): URL {
    return cache.has(s) ? cache.get(s) : s;
    //                                   ~
    // Error: 'string' is not assignable to 'URL'
}

Unless the fix is obvious, treat this as a usercode bug.

Import assertions replaced by import attributes

The assert keyword is no longer accepted; use with:

1
2
- import data from "./data.json" assert { type: "json" };
+ import data from "./data.json" with { type: "json" };

TypeScript 6.0 Migration

TypeScript 6.0 changes many defaults. When upgrading to TS 6.0, you will want to explicitly set any config value that would be inferred differently to avoid unwanted changes.

Setting Old Default New Default Quick Fix
strict false true Add "strict": false to restore old behavior
types ["*"] (all @types) [] (none) Depends
rootDir inferred from sources . (tsconfig directory) Add "rootDir": "./src"
module commonjs esnext Determine correct setting based on project
target es5 es2025 Set to ES6 if the project really needs to target ancient stacks
noUncheckedSideEffectImports false true Set to false to restore old behavior if needed
libReplacement true false Set to true to restore old behavior if needed

During 6.0 migration, you must check rootDir. If all TS files are under src, you must set rootDir since otherwise TS will output to e.g. ./outDir/src instead.

types directives

With types defaulting to [], you may need to add entries to this array depending on the intended global environment:

  • Buffer, modules like "fs", "path", etc: Add "node"
  • describe, it, etc: add the correct test framework ("jest", "mocha", etc)

'rootDir' is expected to contain all source files**

Explicitly add the appropriate rootDir

1
2
3
4
5
 {
   "compilerOptions": {
+    "rootDir": "./src"
   }
 }

Error: Option 'moduleResolution' with value 'node10' is deprecated** (or classic)

For Node.js projects:

1
2
3
4
5
6
 {
   "compilerOptions": {
-    "module": "commonjs"
+    "module": "nodenext"
   }
 }

For web projects using a bundler:

1
2
3
4
5
6
7
8
 {
   "compilerOptions": {
-    "module": "commonjs",
-    "moduleResolution": "node10"
+    "module": "esnext",
+    "moduleResolution": "bundler"
   }
 }

Error: Option 'baseUrl' is deprecated**

If you can simply remove baseUrl, do. However, if the tsconfig has paths entries, run npx -y @andrewbranch/ts5to6 --fixBaseUrl tsconfig.json to fix path mappings automatically

Error: Option 'outFile' is deprecated

Abort and tell the user they need to use their own bundler now.

Error: Option 'module' with value 'amd' is deprecated** (or system, umd):

Abort and tell the user they need to pick a new module format

Option 'esModuleInterop' cannot be set to 'false' (or allowSyntheticDefaultImports)

These are always-on in TypeScript 6.0. Remove the explicit false setting. You may need to update imports:

1
2
- import * as express from "express";
+ import express from "express";

Option ‘downlevelIteration’ is deprecated`

Only had effect on ES5 emit, which is also deprecated. Remove the setting from the config entirely.

TS2307 on private package subpaths

Symptom: After switching moduleResolution to bundler (or node16 / nodenext), imports that reach into a package’s internals fail:

src/utils/zod.ts(3,26): error TS2307: Cannot find module 'zod/v4/core/util.cjs'

Under these resolvers TypeScript honors the package’s exports map, so subpaths that the package does not publish are no longer reachable — even if the file exists on disk.

Fix:

Find the public exports-mapped path that exposes the same symbol. Every well-maintained package publishes types through its exports map — the private subpath exists only for the package’s own internal use. Look in order:

  1. Check the package’s top-level export (import { X } from 'pkg').
  2. Check documented sub-entry-points in the exports field of package.json (e.g. zod/v4, zod/v4/core).
  3. Run node -e "console.log(Object.keys(require('pkg/package.json').exports))" to see all published paths at once.
  4. Search the package’s type declaration files for the symbol name to locate which public entry re-exports it.

Do not fall back to inlining a local copy of the type. That creates drift and hides the real fix. If you cannot locate a public re-export after the steps above, open the package’s repo and search its exports map directly — it is almost always there under a different entry point than the one the code was using.

Do not work around this by disabling exports resolution or downgrading moduleResolution.

updatedupdated2026-06-302026-06-30