
TypeScript 7.0 and 7.1 deep dive: parallelism, new defaults, and migration
A technical deep dive into TypeScript 7.0 and 7.1: how --checkers and --builders work, why the defaults changed, what was removed, migration examples, and FAQ.
In the introductory article on TypeScript 7 we covered the big picture: a compiler rewritten in Go, 8-12x faster builds, a few changed defaults, and a programmatic API that only arrives with 7.1. Here we go one level deeper: how the parallelism works and why it can change results, why the team changed those specific defaults, what concretely breaks in an existing tsconfig.json and how to fix it, and what’s actually in the 7.1 plan.
A port, not a rewrite
The single most important thing to understand about TypeScript 7 is that the Go compiler is not a new type checker. The team ported the existing code file by file, function by function, keeping the same structure: same scanner, same parser, same binder, same checker. That’s also the main reason Go was picked over Rust: the shape of the code (object graphs with cyclic references, a garbage collector, no explicit ownership) translates almost line by line, whereas Rust would have required redesigning the data structures.
The practical consequence is that type semantics are the same as TypeScript 6. If a conditional type, an inference, or a narrowing worked in 6.0, it works the same way in 7.0: the compiler has been validated against the entire test suite accumulated over more than a decade. The differences you’ll see don’t come from “a different checker” but from three specific sources: the new defaults, the removed options, and parallelism.
How parallelism works: --checkers
The old tsc had a single type checker that walked every file in sequence. TypeScript 7.0 instead creates a fixed number of independent type checkers (4 by default), each with its own “view of the world”: every worker gets a slice of the program’s files and checks them in parallel with the others, sharing the already-parsed AST in memory.
# default: 4 checkers in parallel
npx tsc --noEmit
# many-core machine, large codebase
npx tsc --noEmit --checkers 8
# everything on one thread (debugging, low-memory containers)
npx tsc --noEmit --singleThreaded
The trade-off is memory: each checker builds its own types, so “common” types (those from lib.d.ts, React, a library used everywhere) are partly computed more than once. Raising --checkers speeds up large projects but increases RAM usage — on a CI runner with 2 cores and 4 GB, going above the default is often counterproductive.
⚠ Attenzione
Given the same input, files are always split across checkers the same way and results are
deterministic. But changing the number of checkers can, in rare cases, surface order-dependent
results. If your team uses different machines, pin the same --checkers value locally and in CI
(for example in the typecheck script in package.json).
Why stableTypeOrdering is always on
There’s a barely visible detail that’s essential for parallelism to work. In TypeScript ≤ 6, every type got a numeric id in the order it was created, and that id determined, for example, the order of a union’s members. The result: the same type could be printed as string | number or number | string depending on which file happened to be checked first.
With a single sequential checker this was “stable by accident”. With four checkers running in parallel, type creation order is no longer something you can rely on. That’s why TypeScript 7.0 orders types using a criterion that’s independent of checking order: stableTypeOrdering is true and cannot be turned off (in 6.0 it was an opt-in flag to prepare for the migration).
// utils.ts
export function parse(input: string) {
return Number.isNaN(Number(input)) ? input : Number(input);
}
The generated .d.ts contains a union whose order, in 7.0, no longer depends on what got checked first. In practice you’ll notice this change mostly in two places: snapshot tests that compare error messages or generated .d.ts files, and noisy diffs in the declarations a library publishes. Update the snapshots once, and they’ll stay stable from then on.
Project references in parallel: --builders
In monorepos using tsc --build, the second level of parallelism is --builders, which controls how many referenced projects are built at the same time:
npx tsc --build --builders 4
Unlike --checkers, varying the number of builders doesn’t change results. The limit is the dependency graph: if app depends on ui, which depends on core, those three projects stay sequential no matter what. --builders really pays off when you have many independent “leaf” packages.
The new defaults, one by one (and why)
The underlying idea is simple: TypeScript’s defaults were stuck in the ecosystem of ten years ago (ES5, CommonJS, global scripts). The new defaults reflect how TypeScript is written today, so that a minimal tsconfig.json does the right thing.
types: []
Previously, TypeScript automatically included every @types/* package found in node_modules, even ones installed as a transitive dependency of something else. This slowed down program loading and caused conflicts between global declarations (the classic @types/jest vs @types/mocha, both redefining describe). Now no global @types package is included unless you declare it:
error TS2304: Cannot find name 'process'.
error TS2582: Cannot find name 'describe'. Do you need to install type definitions for a test runner?
The fix is to list the global types you actually use:
{
"compilerOptions": {
"types": ["node", "vitest/globals"]
}
}
@types packages you import explicitly (import express from "express" with @types/express) keep working: types only affects global declarations.
rootDir: "./"
Previously rootDir was inferred as the common directory of all source files. That sounds convenient, but it meant the layout of outDir could change just by adding a file: one scripts/seed.ts outside src/ and your output moves from dist/index.js to dist/src/index.js. Now the default is the tsconfig.json directory, and if your sources live in src/ you have to say so:
{
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
},
"include": ["./src"]
}
noUncheckedSideEffectImports: true
Side-effect-only imports (import "./polyfills") weren’t checked: if the file didn’t exist, TypeScript stayed quiet and the error only showed up at runtime or bundling time. Now they’re resolved like any other import:
import "./polyfils"; // error: module not found (typo in "polyfills")
If you import CSS files or other assets this way, you need a module declaration (the Vite and Astro templates already include one):
// src/env.d.ts
declare module "*.css";
strict, module, and target
strict: true was already the first line of almost every template-generated tsconfig.json, so for most projects nothing changes; you’ll only notice it with a minimal or empty config. module: esnext reflects that the typical destination is a bundler or an ESM runtime. target now points at the latest stable ECMAScript version instead of ES5: no down-leveling of classes, async/await, or optional chaining unless you ask for it.
Removed options: a migration table
These options are errors in 7.0, no longer deprecation warnings:
| Before (≤ 6.0) | In TypeScript 7 |
|---|---|
target: "es5", downlevelIteration |
modern target; for ES5 use a transpiler (Babel, SWC) |
moduleResolution: "node" / "node10" |
"bundler" (with a bundler) or "nodenext" (plain Node) |
moduleResolution: "classic" |
"bundler" or "nodenext" |
module: "amd" / "umd" / "systemjs" |
"esnext" + a bundler |
baseUrl |
paths with paths relative to tsconfig.json |
esModuleInterop: false |
remove the option (always on) |
alwaysStrict: false |
remove the option |
module Foo {} |
namespace Foo {} |
import data from "./x.json" assert { … } |
import data from "./x.json" with { type: "json" } |
The most common case in real projects is baseUrl used only for aliases. Before and after:
// before
{
"compilerOptions": {
"baseUrl": "./src",
"paths": { "@/*": ["*"] }
}
}
// after
{
"compilerOptions": {
"paths": { "@/*": ["./src/*"] }
}
}
Watch out: baseUrl also allowed “bare” imports like import { x } from "utils/date" resolved relative to src/. Without baseUrl, those imports need to become explicit aliases (@/utils/date) or relative paths.
Template literals and Unicode
A real semantic change, but one that fixes a long-standing bug. With infer on template literals, TypeScript 6 split strings by UTF-16 code units, cutting emoji and characters outside the Basic Multilingual Plane in half:
type HeadTail<S> = S extends `${infer Head}${infer Tail}` ? [Head, Tail] : never;
type R = HeadTail<"😀abc">;
// TS 6.0: ["\ud83d", "\ude00abc"] — half an emoji on each side
// TS 7.0: ["😀", "abc"]
If you have utility types that iterate over strings character by character (type-level string length, format validators), results change for non-ASCII input — in the correct direction.
JavaScript projects: JSDoc support has been cleaned up
If you use TypeScript to check .js files via JSDoc, 7.0 drops a set of legacy patterns the old compiler accepted for Closure Compiler compatibility:
// ❌ no longer supported
/** @type {function(string): void} */ // Closure-style syntax
/** @type {?} */ // standalone "?" as a type
/** @enum {string} */ // special @enum handling
/** @param {Foo!} x */ // postfix "!"
// ✅ equivalents
/** @type {(s: string) => void} */
/** @type {any} */
/** @param {Foo} x */
Using a value where a type is expected is no longer accepted either: if Config is a variable, in JSDoc you have to write typeof Config.
CLI, watch mode, and editors
Two practical details. First: tsc file.ts in a directory containing a tsconfig.json now fails, because it was ambiguous whether the config should apply. If you really want to compile a single file ignoring the config:
npx tsc --ignoreConfig scripts/one-off.ts
Second: watch mode was rebuilt on Parcel’s file watcher (ported to Go), which is more stable and less CPU-hungry than polling. In the editor, the new language server cuts failed commands by over 80% and crashes by over 60% compared to 6.0; in VS Code it’s enabled through the native extension, and you can switch it off with the “Disable TypeScript 7 Language Server” command if a project still needs plugins built on the old API.
The API gap and living with 6.0
TypeScript 7.0 doesn’t expose a programmatic API. Anything that imports typescript as a library — typescript-eslint, Volar for Vue, the Svelte, Astro, and MDX plugins, Angular templates, many code generation tools — has to stay on 6.0. That’s what the @typescript/typescript6 package (binary tsc6) is for, used via npm aliases:
{
"devDependencies": {
"@typescript/native": "npm:typescript@^7.0.2",
"typescript": "npm:@typescript/typescript6@^6.0.2"
},
"scripts": {
"typecheck": "tsc -p . --noEmit --checkers 4"
}
}
With this setup, tools that require("typescript") get 6.0, while you can use the 7.0 binary for fast type-checking in CI. Two compilers, two versions: keep them on the same configuration and treat 6.0 as a temporary bridge.
Roadmap: from “Corsa” to TypeScript 7.1
To put everything in context, here are the project’s main milestones, from the announcement of the native port to the planned 7.1 dates:
| Date | Milestone | What it means |
|---|---|---|
| March 2025 | Native port announced (“Corsa”) | The team unveils the Go compiler and the goal of ~10x faster builds |
| May 2025 | Native Previews | @typescript/native-preview on npm (tsgo binary) and a preview VS Code extension |
| December 2025 | Progress update | Project reference builds and the language service close to feature-complete |
| February 11, 2026 | TypeScript 6.0 Beta | The last release built on the JavaScript codebase, designed as a bridge to 7.0 |
| March 23, 2026 | TypeScript 6.0 stable | Deprecations and flags like stableTypeOrdering to prepare for the migration |
| April 21, 2026 | TypeScript 7.0 Beta | The native compiler ships inside the typescript package |
| June 18, 2026 | TypeScript 7.0 RC | Behavior considered final, only critical bug fixes from here |
| July 8, 2026 | TypeScript 7.0 stable (7.0.2) | New defaults, legacy options removed, no programmatic API |
| October 6, 2026 | TypeScript 7.1 Beta (planned) | First stable APIs (Content Mapper, Emit, Language Service), es2026 |
| November 10, 2026 | TypeScript 7.1 RC (planned) | Feature freeze |
| November 24, 2026 | TypeScript 7.1 stable (planned) | Migration becomes possible for Vue, Svelte, Astro, Angular, and typescript-eslint too |
The 7.1 dates come from the iteration plan published on GitHub and may still change; all the others are actual release dates.
TypeScript 7.1: what’s in the plan
7.1 has its beta planned for October 6, RC for November 10, and stable for November 24, 2026. The iteration plan published on GitHub splits into four areas.
Stable APIs. This is the heart of the release. Three pieces:
- Content Mapper API — lets a tool tell the compiler “this
.vue/.astro/.sveltefile corresponds to this virtual TypeScript code, with these position mappings”. That’s exactly what Volar and the framework plugins do today, and the reason they can’t move to 7.0 yet. - Emit API — for anyone using the compiler to produce output (custom transformers, code generation).
- Language Service API — completions, hover, rename, diagnostics: the foundation for editors and linters.
The explicit goal is to let Angular, Vue, Svelte, and typescript-eslint use the native compiler, and to replace the existing integrations in VS Code.
Language and lib. A new es2026 option for target and lib; support for type in import attributes; support for source phase imports, the TC39 proposal that lets you import a module’s “source” without evaluating it — the main use case is WebAssembly:
import source wasmModule from "./image-filter.wasm";
// wasmModule is a compiled but not yet instantiated WebAssembly.Module
const instance = await WebAssembly.instantiate(wasmModule, imports);
Declarations also land for the new iterator methods and for Promise.allKeyed/Promise.allSettledKeyed, which do what Promise.all does with arrays, but on an object with keys:
const { user, posts } = await Promise.allKeyed({
user: fetchUser(id),
posts: fetchPosts(id),
});
// no more positional [user, posts] destructuring to keep in sync
Performance. On top of what 7.0 already delivered, the plan lists targeted optimizations: faster union type construction, fast paths for narrowing on assignment, equality, and switch/case over unions, more compact representations of the checker’s internal tables, and above all experiments on how to distribute files across checkers — in other words, reducing the duplicated work discussed above.
Infrastructure. A WebAssembly (wasip1) build of the compiler, meant among other things to run the Playground in the browser with the new compiler, plus Android ARM64 builds.
ℹ️ Nota
An iteration plan is a plan, not a promise: some items are marked “investigate” (for example support for Node 26 package maps) and may slip. As of this writing, 7.1 isn’t in beta yet.
Migration checklist
- Install
typescript@7on a branch and runnpx tsc --noEmit: configuration errors show up immediately, before type errors. - Fix removed options using the table above (
baseUrl,moduleResolution: node,target: es5). - Explicitly add
typesfor the globals you use (node, your test runner). - Set
rootDirif your sources aren’t at the project root, and double-check theoutDirlayout. - Add
declare moduledeclarations for binding-less asset imports. - Update snapshots containing error messages or generated
.d.tsfiles. - If you use typescript-eslint or a framework with non-
.tsfiles, set up the@typescript/typescript6alias until 7.1. - Pin
--checkersin your type-check script so local and CI behave the same way.
FAQ
❓ Does TypeScript 7 change how I write types?
No. 7.0 is a port of the existing compiler: same syntax, same inference, same narrowing. The
differences you’ll run into come from the new tsconfig.json defaults, the removed options, and a
few edge cases like Unicode handling in template literals.
❓ Why might different --checkers values produce different errors?
Each checker builds types independently over its own slice of files. With the same number of checkers the file split is always identical and results are deterministic; change that number and the split changes, which in rare cases can surface order-dependent behavior. Using the same value locally and in CI removes the issue.
❓ How many checkers should I use?
The default (4) is a good starting point. On many-core machines with large codebases, 6-8 can cut times further at the cost of more memory. On small CI runners (2 cores, little RAM) it’s better to stay at the default or go lower.
❓ Can I still emit ES5 JavaScript?
Not with tsc: target: es5 and downlevelIteration have been removed. If you need to support
very old environments, compile with TypeScript to a modern target and then down-level with Babel
or SWC.
❓ Does typescript-eslint work with TypeScript 7?
Not directly yet, because it relies on the programmatic API coming in 7.1. In the meantime,
install @typescript/typescript6 as an alias for the typescript package, so the linter uses 6.0
while CI type-checking uses the native compiler.
❓ Should I wait for 7.1?
It depends on your stack. For a plain Node or React project, 7.0 is already usable and the speed gain is immediate. If you use Vue, Svelte, Astro, MDX, or Angular templates, 7.1 is the release that makes a full migration possible, editor included.
Conclusion
TypeScript 7.0 is a release best understood by looking at what doesn’t change: the language. Everything else — parallelism, stable type ordering, modern defaults, removed legacy options — exists to run the same type checker much faster and to drop a decade of compatibility with environments almost nobody targets anymore. 7.1 closes the loop by giving the API back to the ecosystem.
If you haven’t read the overview yet, start with the article on TypeScript 7 and 7.1.

Co-Founder & CTO at PAPION. Senior full-stack engineer specializing in React, TypeScript, Node.js, and application security.