DEV Community

Cover image for 5 TypeScript Defaults That Quietly Changed in 2026 (And Why Your Build Is Now Broken)
Gabriel Anhaia
Gabriel Anhaia

Posted on

5 TypeScript Defaults That Quietly Changed in 2026 (And Why Your Build Is Now Broken)


You bumped TypeScript from 5.x to 6.0 in a Renovate PR. The PR was green on your laptop. CI was green on a fresh checkout. The diff was four characters in package.json. You merged.

Then the deploy queue lit up.

> tsc --noEmit
src/util/iter.ts:9:5
  error TS7034: Variable 'rows' implicitly has type 'any[]'...
src/billing/cents.ts:42:18
  error TS7006: Parameter 'invoice' implicitly has an 'any' type.
Found 117 errors in 41 files.
Enter fullscreen mode Exit fullscreen mode

None of that is a bug in your code. It is the new default surface. TypeScript 6.0 ships with a tsconfig that assumes a 2026 codebase, not a 2014 one, and it announces that opinion by failing loudly the moment your assumptions don't match.

Here are the five defaults that flipped, in order of how often they will wake somebody up.

Five labelled gears in a row, four spinning, one sparking with a small

1. strict is now true by default

This is the one everybody saw coming and nobody prepared for.

For a decade, every "TypeScript best practices" post has ended with the same line: turn on strict. Half the ecosystem did. The other half kept shipping with strict: false, often without realizing it, because their tsconfig inherited from a yeoman generator written in 2017.

In 6.0, the half that kept it off becomes the half that stops compiling.

What it was: omitting strict defaulted to false. noImplicitAny, strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, alwaysStrict, noImplicitThis, and useUnknownInCatchVariables all defaulted to off.

What it is now: omitting strict defaults to true. All eight sub-flags activate together. The errors you get on the first build are not new — the compiler just stopped pretending they weren't.

What breaks: implicit any everywhere. Functions typed as returning a string that quietly returned null. Catch clauses where error was implicitly any and you called .message on it. Class fields declared without an initializer and assigned later in componentDidMount. The breakage is rarely subtle.

The one-line fix, if you need to ship today and audit tomorrow:

// tsconfig.json
{
  "compilerOptions": {
    "strict": false
  }
}
Enter fullscreen mode Exit fullscreen mode

The honest fix is to keep strict on, fix the errors, and treat the diff as the audit you have been postponing since 2019. useUnknownInCatchVariables hurts the most because it touches every try block, but a codemod that rewrites catch (e) to catch (e: unknown) plus a narrowing helper closes 80% of those errors in one pass.

For a phased rollout, set strict: true at the root and override per-folder with project references. Don't sprinkle // @ts-nocheck across the tree.

2. module defaults to esnext

The 2014-vs-2026 version: in 2014 you compiled to CommonJS because Node didn't speak ESM. In 2026 you compile to ESM because Node, Bun, Deno, every bundler, and every browser do.

TypeScript 6.0 made that the default. module now defaults to esnext. The amd, umd, and systemjs values are deprecated. module: none is gone.

The emitter stops producing exports.foo = .... It produces export const foo = .... If your build was a tsc → CommonJS → bundler chain, the middle step changes shape. Vite, Rspack, esbuild, and Bun prefer ESM input anyway. The configurations that break: ts-node without ESM loader flags, Jest without --experimental-vm-modules, anything still going through babel-plugin-transform-modules-commonjs after tsc.

The error you'll see in the wild is the one where a CommonJS test runner tries to require() a file that now ships as ESM. The stack trace is the giveaway:

Error [ERR_REQUIRE_ESM]: require() of ES Module
/app/dist/util/cents.js from /app/test/cents.test.js not supported.
Instead change the require of cents.js to a dynamic import().
Enter fullscreen mode Exit fullscreen mode

If you genuinely need CommonJS output (publishing a dual package, or running on a Node version with a strict CJS-only consumer), be explicit:

{
  "compilerOptions": {
    "module": "commonjs",
    "moduleResolution": "bundler"
  }
}
Enter fullscreen mode Exit fullscreen mode

moduleResolution: node and node10 are both deprecated in 6.0. With module: commonjs the new default for moduleResolution is bundler; use nodenext if you want strict Node ESM/CJS dual-format semantics. Pinning module: commonjs without thinking about the matching resolution mode is how you end up with import paths that resolve in dev and 404 in prod.

3. target defaults to es2025

target: "es2025". As a default. In a config you didn't write.

Read that twice if it stings. It is the change with the largest blast radius for anyone shipping to a browser fleet older than a 2024 evergreen.

Before 6.0: target defaulted to es3. Yes, ES3 — frozen since the heat death of Internet Explorer 6. Nobody actually used it; every framework template overrode it.

After 6.0: target defaults to the current-year ECMAScript spec — es2025 at release, and effectively floating as new spec versions land. The release notes mark target: "es5" as deprecated; the compiler emits a suppressible diagnostic you can silence with ignoreDeprecations: "6.0" for one major. TypeScript 7 is expected to remove suppression for 6.0-deprecated options per the published deprecation plan.

What breaks:

  1. You ship to old Safari, old Android WebView, or anything embedded — the emitted output contains optional chaining, nullish coalescing, top-level await, class fields, and decorators in their final form. Older runtimes fail to parse it before they fail to execute it.
  2. Your bundler was doing a second downlevel pass (Babel after tsc) configured against the old assumption.
  3. Your published library has consumers on Node 14. Node 14 cannot parse class { #x = 1 } correctly under all conditions.

The fix, if any of those describe you, is to be explicit about the floor:

{
  "compilerOptions": {
    "target": "es2020"
  }
}
Enter fullscreen mode Exit fullscreen mode

es2020 is the modern conservative floor: Node 14+, every evergreen browser since 2020, most embedded V8 forks. If you have to go lower than that, you are fighting the language, and that fight is now on a clock: es5 is deprecated for one version, then gone.

The honest answer for most teams is to stop downleveling entirely. Ship es2025 to the 99% and a separate legacy bundle to the 1%. The byte savings beat forcing every payload through a transpiler aimed at the lowest common runtime.

Stack of tsconfig settings, four labelled

4. types defaults to []

This one will not break your build. It will subtly change what is in scope.

Until 6.0, TypeScript walked node_modules/@types, picked up every package, and added their global declarations to your project automatically. @types/jest, @types/node, @types/react-router-dom, and @types/some-thing-a-coworker-installed-three-years-ago were all implicitly in the global namespace. Convenient, until your editor autocompleted describe( in a file that had nothing to do with tests, Buffer showed up as a global in your browser code, and removing a dev dependency mysteriously broke unrelated source files.

In 6.0, types defaults to []. Nothing in @types is auto-loaded. You opt in by name.

The TypeScript release notes report 20–50% build-time improvements on projects that set types explicitly — the new default makes that the baseline.

What this breaks: any file that relied on a transitively-installed @types package being globally available. The classic one is @types/node — if you used process.env, Buffer, setTimeout (under DOM lib it is fine; under non-DOM lib it isn't), or __dirname and you weren't importing them explicitly, those are now Cannot find name errors.

The fix is one line:

{
  "compilerOptions": {
    "types": ["node"]
  }
}
Enter fullscreen mode Exit fullscreen mode

For a Vite app with React, you want ["node", "vite/client"]. For Jest, add "jest". Next.js's own preset already handles this — if your tsconfig extends a generic base, add explicit entries.

The new default is the right one. The old default was a bug pretending to be a feature. Quietly inflating your global namespace based on whatever sat in node_modules/@types is not convenience; it is action at a distance.

5. esModuleInterop is locked on; baseUrl and target: es5 are deprecated

Three small ones, grouped because they all hit the same kind of codebase: the one with a tsconfig that has been edited continuously since 2018.

esModuleInterop can no longer be set to false. If you had esModuleInterop: false (often inherited from a 2018 starter), remove the line. The default is now equivalent to the old true, with no opt-out.

baseUrl is deprecated. For years, baseUrl: "." was how teams enabled non-relative imports like import { foo } from "src/util/foo". The 6.0 release notes mark it deprecated; TypeScript 7 removes it. The replacement is paths:

{
  "compilerOptions": {
    // Don't:
    // "baseUrl": ".",

    // Do:
    "paths": {
      "@/*": ["./src/*"],
      "@util/*": ["./src/util/*"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

If you cannot migrate today, add "ignoreDeprecations": "6.0" and put a calendar reminder for the TypeScript 7 RC. That escape hatch is per-version.

target: "es5" now emits a deprecation diagnostic; suppress it with ignoreDeprecations: "6.0" if you need to keep the build green this cycle. --downlevelIteration lost most of its purpose along with it. If you actually need ES5 output, set up a separate Babel pass after tsc; the target flag is no longer the lever.

Your old tsconfig comment audit

Here is the part nobody wants to do, and the part that gives back the most.

Most production tsconfigs accumulate settings the way attic boxes accumulate cables. A flag was load-bearing in 2019, a tooling change made it redundant, nobody removed it because the build was green. Six years later your tsconfig has 38 fields and 11 of them affect output.

After a 6.0 upgrade, do one pass with this question for every line: if I delete this, does anything actually change?

These are the settings that were once load-bearing and are now noise on a 6.0 baseline:

{
  "compilerOptions": {
    // Now the default — delete.
    "strict": true,

    // Now the default — delete.
    "esModuleInterop": true,

    // Now the default — delete.
    "module": "esnext",

    // Now the default — delete.
    "target": "es2025",

    // Inferred from your input files now — delete unless you're forcing a different root.
    "rootDir": ".",

    // Removed in 5.5 — delete.
    "suppressImplicitAnyIndexErrors": true,

    // Removed; was a no-op for years before 6.0 — delete.
    "keyofStringsOnly": true,

    // Removed; was a no-op since 4.5 — delete.
    "suppressExcessPropertyErrors": true,

    // Removed; outFile was killed in 6.0 — delete.
    "outFile": "./bundle.js",

    // Was always implied under strict mode — delete.
    "alwaysStrict": true,

    // Inferred from your moduleResolution now — delete.
    "allowSyntheticDefaultImports": true,

    // moduleResolution: classic was removed; if you still have it, replace with "bundler" or "nodenext"
    "moduleResolution": "classic"
  }
}
Enter fullscreen mode Exit fullscreen mode

Run that audit once and your tsconfig drops from 30+ lines to roughly 8. The remaining eight encode a real decision: paths, lib, output directory, composite if you use project references, jsx, types for the new explicit list, noEmit if tsc isn't your emitter, and whatever experimental flag your codebase actually uses.

A small tsconfig is a documentation artifact: every line is a deliberate override of a default.

What this means for the next year

TypeScript 6.0 is the last major before the Go-rewritten compiler ships in 7.0. The 6.0 deprecations give you one upgrade window — through 6.x — to fix what 7.0 will reject outright. ignoreDeprecations: "6.0" is the escape hatch, and it is dated.

Codebases that stay close to the defaults will be cheap to upgrade in 2027. The ones pinned to the 2018 shape will not.

If this was useful

The full upgrade path — paths vs baseUrl, dual ESM/CJS publishing, tsconfig for monorepos, library authoring across Node, Bun, and the browser — is what TypeScript in Production (book 5 in The TypeScript Library) is for. If you found yourself nodding at the tsconfig audit section, that is roughly half the book.

The other four books in the collection cover the language itself, depending on where you are coming from:

  • TypeScript Essentials — the entry point. Types, narrowing, modules, async, daily-driver tooling.
  • The TypeScript Type System — the deep dive. Generics, mapped and conditional types, infer, template literals, branded types.
  • Kotlin and Java to TypeScript — the bridge for JVM developers. Variance, null safety, sealed unions, coroutines to async/await.
  • PHP to TypeScript — the bridge for modern PHP 8+ developers. Sync to async, generics, discriminated unions.

The TypeScript Library — a 5-book collection on TypeScript across Node, Bun, Deno, and the browser

Top comments (1)

Collapse
 
pengeszikra profile image
Peter Vivo

Important information, thx for sharing.