Why TypeScript 6.0 Changed the Defaults — strict by Default, using Declarations, Temporal API, and the Last JS Compiler
In March 2026, TypeScript 6.0 was released. It wasn't just a version number bump — it was a declaration that "TypeScript is strict by default from here on out." Strict mode became the default, the compilation target moved up to ES2025, and years of accumulated legacy options were cleaned up all at once. On top of that, this version is also the last release of the TypeScript compiler written in JavaScript. The next version, TypeScript 7.0, is being completely rewritten in Go (Project Corsa). In this article, we'll walk through the strict default change, using declarations, Temporal API, subpath imports, and the transition to a Go-based 7.0.
This article is written for teams already using TypeScript. If you haven't adopted TypeScript yet, 6.0 and later — with strict as the default — is actually a great time to start. Beginning in a strict environment from day one means you won't have to pay down that debt later.
When I first read the release notes, I thought "eh, how big a deal could it be?" — but when I actually upgraded an internal project, more than 30 type errors came pouring out. That's when I truly felt how deeply embedded the reliance on strict: false was in existing code. Drawing on that experience, I'll also cover what to watch out for during migration.
Core Concepts
strict Mode Is Finally the Default
Honestly, this is the most impactful change. Up until now, if you started a new TypeScript project and didn't explicitly add "strict": true to your tsconfig.json, the following checks were silently disabled.
| Check | Description |
|---|---|
strictNullChecks |
null/undefined must be handled explicitly |
noImplicitAny |
Implicit any types are prohibited |
strictFunctionTypes |
Enforces type safety for function parameter types |
useUnknownInCatchVariables |
Variables in catch blocks are typed as unknown |
strictPropertyInitialization |
Class properties must be initialized |
strictFunctionTypes might be unfamiliar, so here's a quick explanation — it catches situations like this:
// strictFunctionTypes enabled
type Handler = (value: string | number) => void;
function handleString(value: string) {
console.log(value.toUpperCase()); // only uses string methods
}
// Handler can receive number too, but handleString only handles string
// → if a number comes in at runtime, it could blow up — type error!
const handler: Handler = handleString; // Error!Now all of these checks are enabled by default even if you write nothing in tsconfig.json. That's great for new projects, but upgrading an existing project to 6.0 can trigger a flood of type errors all at once.
The Default Compilation Target Has Been Raised to ES2025
The fact that TypeScript's default target was ES3 was honestly a bit absurd. It meant we were generating code for Internet Explorer by default in 2026.
// Default before 6.0 (in effect)
{
"compilerOptions": {
"target": "ES3", // roughly early 2000s level
"module": "commonjs",
"strict": false
}
}// Default from 6.0 onward
{
"compilerOptions": {
"target": "ES2025", // based on modern runtimes
"module": "esnext",
"moduleResolution": "nodenext",
"strict": true
}
}ESM (ECMAScript Module): The standard JavaScript module system using
import/exportsyntax. Unlike the traditional CommonJS (require()), it supports static analysis and native browser support.
What It Means to Be the Last JavaScript-Based Release
TypeScript 7.0 is being completely rewritten using Go under the name "Project Corsa." The reason Microsoft chose Go over Rust is that the TypeScript compiler's shared mutable state and recursive type inference structure maps more naturally to Go.
The performance improvement figures from Microsoft's published internal benchmarks are quite impressive.
| Project | Before (JS) | TypeScript 7.0 (Go) | Speedup |
|---|---|---|---|
| VS Code (1.5M LOC) | 89s | 8.74s | 10.2× |
| Sentry | 133s | 16s | 8.2× |
| Playwright | 11.1s | 1.1s | 10.1× |
| TypeORM | 17.5s | 1.3s | 13.5× |
This is exactly why 6.0 is doing a major cleanup of legacy options — it's groundwork to minimize technical debt when migrating to the 7.0 Go compiler. Note that there are no plans for 6.x minor releases; 6.0 is the only 6.x release.
Practical Application
Example 1: Eliminating Resource Leaks with using Declarations
The using declaration was first introduced in TypeScript 5.2 (August 2023) and became more firmly established as a recommended pattern in 6.0. Tied to the ECMAScript Explicit Resource Management proposal, it lets you safely clean up resources without try-finally.
This is a situation that comes up often in practice — opening a file or DB connection and then failing to clean up when an exception occurs is more common than you'd think.
// Old way — resource leak if you forget finally
function processFile() {
const file = openFile('data.txt');
try {
doWork(file);
} finally {
file.close(); // forget this and you're done
}
}
// using declaration — [Symbol.dispose]() called automatically when scope exits
function processFile() {
using file = openFile('data.txt'); // always closed, whether exception or normal exit
doWork(file);
}For the using keyword to work, the object must implement the [Symbol.dispose]() method. Most built-in Node.js APIs don't directly support this yet, so you'll need to create your own wrappers or use a library. Here's how you can implement it:
import * as fs from 'fs';
// File handle wrapper implementing [Symbol.dispose]()
function openFile(path: string) {
const handle = fs.openSync(path, 'r');
return {
read(buffer: Buffer, offset: number, length: number, position: number) {
return fs.readSync(handle, buffer, offset, length, position);
},
[Symbol.dispose]() {
fs.closeSync(handle);
}
};
}
// DB connections work the same way
class DatabaseConnection {
constructor(private conn: Connection) {}
query(sql: string) {
return this.conn.execute(sql);
}
[Symbol.dispose]() {
this.conn.close();
}
}
async function queryData() {
using db = new DatabaseConnection(await connect());
return await db.query('SELECT * FROM users');
// db.conn.close() is called automatically the moment the block exits
}For async cleanup, you can use await using with [Symbol.asyncDispose]().
[Symbol.dispose]: A well-known Symbol added to the ECMAScript standard. Theusingdeclaration automatically calls this method when leaving scope to clean up resources. It's analogous to C#'sIDisposableor Python's__exit__.
Example 2: Escaping Path Hell with #/ Subpath Imports
Relative path imports like ../../utils/formatDate become a real maintenance headache when you move files around. Node.js's subpath imports feature has been supported since version 12.7, and TypeScript has had type support since 4.7 — but in TypeScript 6.0, IDE autocomplete has been significantly improved. Autocomplete and go-to-definition for paths starting with #/ now work much more naturally.
The key is that the imports field in package.json and the moduleResolution setting in tsconfig.json need to align. It's important to look at both files together.
// package.json — defines subpath imports
{
"name": "my-app",
"imports": {
"#/*": "./dist/*",
"#utils/*": "./src/utils/*",
"#lib/*": "./src/lib/*"
}
}// tsconfig.json — moduleResolution must be nodenext or bundler
{
"compilerOptions": {
"moduleResolution": "nodenext",
"module": "esnext"
}
}// Old way — must update manually when file location changes
import { formatDate } from '../../../../utils/formatDate';
import { apiClient } from '../../../lib/api/client';
// Subpath imports
import { formatDate } from '#utils/formatDate';
import { apiClient } from '#lib/api/client';Note that the --baseUrl option has been removed in 6.0. If you're currently using the pattern of baseUrl: "." combined with paths, consider migrating to subpath imports or resetting paths to relative paths based on the tsconfig.json file. For the exact migration steps, check the official release notes linked in the references below.
Example 3: Safe Date Calculations with Temporal API
We all know how painful JavaScript's Date object is. I used to think it was perfectly natural to reach for moment.js or date-fns every time I had to deal with timezone-mixed date calculations — but with Temporal API, that need is going to shrink dramatically.
In TypeScript 6.0, you can use Temporal's type definitions via the esnext.temporal lib option.
// tsconfig.json
{
"compilerOptions": {
"lib": ["esnext", "esnext.temporal"]
}
}// Date calculations — full type support
const now = Temporal.Now.plainDateISO();
const deadline = Temporal.PlainDate.from('2026-12-31');
const duration = now.until(deadline);
console.log(`${duration.days} days until the deadline`);
// Timezone handling is intuitive too
const seoulTime = Temporal.Now.zonedDateTimeISO('Asia/Seoul');
const nyTime = seoulTime.withTimeZone('America/New_York');
console.log(nyTime.toString());⚠️ Runtime Warning: Temporal has just entered TC39 Stage 4 and may not yet be fully supported without a flag in major browsers and Node.js. Even if you can write code without type errors in TypeScript, you may encounter a
Temporal is not definederror at runtime. For real usage, it's recommended to install@js-temporal/polyfillalongside it.
Example 4: Handling CSS Import Errors
With noUncheckedSideEffectImports now enabled by default, simply importing CSS in Vite or webpack projects will throw an error. My first reaction was "why did CSS imports suddenly break?" — but adding an ambient module declaration fixes it.
// TS2882 error after upgrading to 6.0
import './style.css'; // Error: File not found
import 'font-awesome/css/font-awesome.css'; // Same error// Fix: add to global.d.ts or vite-env.d.ts
declare module '*.css';
declare module '*.scss';
declare module '*.sass';
declare module '*.less';
declare module '*.svg';
declare module '*.png';For Vite projects, adding this to the vite-env.d.ts that's generated by default keeps things clean.
Pros and Cons Analysis
Advantages
| Item | Details |
|---|---|
| Improved type safety | strict defaults mean no-gap type checking applied from new projects onward |
| Modern ecosystem alignment | ES2025 target, esnext module, and nodenext resolution match the 2026 reality |
| Resource leak prevention | using declarations structurally guarantee cleanup logic |
| 7.0 migration readiness | Completing the 6.0 transition makes it easier to move to Go-based 7.0 at 10× speed |
| Reduced bundle size | Code cleanup from removing legacy AMD/UMD/SystemJS module formats |
Disadvantages and Caveats
| Item | Details | Mitigation |
|---|---|---|
| Existing project build breaks | strict defaults cause a flood of type errors all at once | Explicitly set "strict": false in tsconfig, then fix incrementally |
| Legacy option removal | moduleResolution: "classic", --outFile, --baseUrl removed |
Use the ts5to6 tool for automatic tsconfig migration |
| CSS import errors | noUncheckedSideEffectImports enabled by default |
Add ambient module declarations to global.d.ts |
| ES5 target support dropped | Hard to support legacy projects targeting IE | Requires separate transpiler setup such as Babel |
esModuleInterop forced |
Cannot be set to false; affects mixed CJS/ESM projects | Review and update import styles |
| Temporal not supported at runtime | Types work but runtime errors are possible | Install @js-temporal/polyfill alongside |
esModuleInterop: An option that ensures compatibility when importing CommonJS modules using
importsyntax. It enables default imports likeimport React from 'react'. From 6.0 onward, it is permanently enabled.
Most Common Mistakes in Practice
-
Trying to fix all strict errors at once and burning out — When upgrading an existing project, the far more realistic approach is to first explicitly set
"strict": falsein tsconfig to get the build back up, then gradually enable individual strict options file by file. I've often seen teams burn out wrestling with dozens of errors all at once. -
Trying to use
#/subpath imports without the moduleResolution setting — It won't work with the oldermoduleResolution: "node"setting. You must change it to"nodenext"or"bundler"first. -
Trying to use the Temporal API without the lib setting — In some projects, setting only
"target": "esnext"doesn't automatically load Temporal types. It's safer to explicitly add"lib": ["esnext", "esnext.temporal"], and don't forget the runtime polyfill either.
Closing Thoughts
TypeScript 6.0 is a release that redefines what it means to use TypeScript from here on out. Yes, there's a short-term migration cost, but once you get through it, a strict environment will feel natural — and when Go-rewritten TypeScript 7.0 arrives, you'll be ready to meet it.
Here's a breakdown of where to start based on your situation:
For existing projects:
- Run
npx ts5to6— The official tool built by TypeScript team member Andrew Branch. It automatically analyzes your current tsconfig and migrates items that need to change. It also handles monorepos and project references environments well. - Prioritize getting the build back up — If there are many strict errors, it's recommended to explicitly set
"strict": falsein tsconfig first, then fix things incrementally according to your team's schedule.
For new projects: 3. Start with 6.0 defaults as-is — You can start with strict, ES2025, and nodenext all enabled without any extra tsconfig configuration. Developing in a strict environment from the beginning means you'll never have to pay down legacy debt later.
After reading through this article, you'll be able to see TypeScript 6.0's changes not as "annoying migration issues" but as "an inevitable cleanup in preparation for the 7.0 era."
References
- Announcing TypeScript 6.0 | Microsoft DevBlogs
- TypeScript 6.0 Release Notes | typescriptlang.org
- Announcing TypeScript 6.0 RC | Microsoft DevBlogs
- Announcing TypeScript 6.0 Beta | Microsoft DevBlogs
- TypeScript 5.x to 6.0 Migration Guide | GitHub Gist (privatenumber)
- TypeScript 6.0 Is Here, And Microsoft Is Rebuilding the Entire Compiler in Go | Nandann
- TypeScript 7.0 and Project Corsa: The Go Rewrite Guide | CodeWithSeb
- What's New in TypeScript 6.0 | OpenReplay
- TypeScript 6.0 and CSS Side-Effect Imports | Schalk Neethling
- TypeScript 6.0 vs 5.x: Complete Migration Guide | DevBolt