Development How-to

Move to TypeScript 7 in two passes: get clean on 6.0, then diff the two compilers

TypeScript 7.0 turns a pile of 6.0 deprecations into hard errors and ships no programmatic API. A short script that runs both compilers and diffs their errors turns the upgrade into a reviewable change. About 20 minutes on one project.

Two identical engine blocks side by side, one hatched in peach and one filled with blue halftone dots, overlapping in a bright seam

A project that compiles cleanly on TypeScript 6.0 can still fail on 7.0, and the reason is almost always one line in tsconfig.json. 7.0 turned the options 6.0 deprecated into hard errors, changed what rootDir and types default to, and shipped without a programmatic API. None of that is hard to fix. The risk is finding it one CI failure at a time, or handing the whole thing to an agent that “fixes” errors by loosening the config.

There’s a cheaper route. Get the project clean on 6.0 with the deprecation silencer removed, then run both compilers side by side and diff what they report. An empty diff is the evidence the upgrade changed nothing. A non-empty one is the review.

The technique needs a shell and a compiler, so it works with any coding agent or none. The prompts below are recommendations; the commands and output are real.

Install both compilers side by side

TypeScript 7.0 (released 8 July 2026) is the default typescript package on npm, and its binary is tsc. The 6.0 line lives on as @typescript/typescript6, which provides a tsc6 binary and still exports the 6.0 API. The TypeScript team’s own suggestion for tools that import typescript (typescript-eslint is the usual one) is to alias them:

{
  "devDependencies": {
    "@typescript/native": "npm:typescript@^7.0.2",
    "typescript": "npm:@typescript/typescript6@^6.0.2"
  }
}

With that in place, npx tsc runs 7.0 and npx tsc6 runs 6.0, while anything that does import ts from "typescript" still gets the 6.0 API. Check it:

npx tsc --version
npx tsc6 --version
Version 7.0.2
Version 6.0.3

The alias matters because 7.0 ships no API at all. Anything that drives the compiler programmatically (linters with type-aware rules, codegen, some bundler plugins) needs 6.0 until the tool moves. The Vue, MDX, Astro and Svelte language tooling is in the same position, and so is Angular template checking, according to the 7.0 announcement. If the project uses any of those, keep 6.0 for the editor and use 7.0 for tsc in CI.

Pass one: make 6.0 stop warning

Most upgrade pain is a deprecation 6.0 was already reporting. The catch is that "ignoreDeprecations": "6.0" hides them. Take a config of the kind that’s been sitting in a repo since 2019:

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "moduleResolution": "node",
    "baseUrl": "./src",
    "outDir": "dist",
    "rootDir": "./src",
    "esModuleInterop": true,
    "strict": false,
    "types": ["node"]
  },
  "include": ["src"]
}

Delete ignoreDeprecations if it’s there, and run 6.0:

npx tsc6 -p tsconfig.json --noEmit
tsconfig.json(3,15): error TS5107: Option 'target=ES5' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '"ignoreDeprecations": "6.0"' to silence this error.
tsconfig.json(5,25): error TS5107: Option 'moduleResolution=node10' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '"ignoreDeprecations": "6.0"' to silence this error.
  Visit https://aka.ms/ts6 for migration information.
tsconfig.json(6,5): error TS5101: Option 'baseUrl' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '"ignoreDeprecations": "6.0"' to silence this error.
  Visit https://aka.ms/ts6 for migration information.

That’s the whole to-do list for the config, and it matches what 7.0 rejects. Run the same file through 7.0 and the warnings become removals:

tsconfig.json(3,15): error TS5108: Option 'target=ES5' has been removed. Please remove it from your configuration.
tsconfig.json(5,25): error TS5108: Option 'moduleResolution=node10' has been removed. Please remove it from your configuration.
tsconfig.json(6,5): error TS5102: Option 'baseUrl' has been removed. Please remove it from your configuration.
  Use '"paths": {"*": ["./src/*"]}' instead.

The replacements are choices, and they are the part worth reading yourself:

  • target: es5 has no equivalent. Pick the lowest runtime you ship to. If a bundler or Babel does the downlevelling, es2022 or later is normal.
  • moduleResolution: node becomes bundler for code that a bundler consumes, or nodenext for code that runs directly in Node. nodenext also requires file extensions on relative imports, so it can mean touching every import.
  • baseUrl becomes a paths entry, as the error message says. Bare imports like import { total } from "lib/money" keep working under bundler with "paths": { "*": ["./src/*"] }. They fail under nodenext, so that choice has consequences.

A config that passes both:

{
  "compilerOptions": {
    "module": "esnext",
    "moduleResolution": "bundler",
    "paths": { "*": ["./src/*"] },
    "outDir": "dist",
    "rootDir": "./src",
    "strict": true,
    "types": ["node"]
  },
  "include": ["src"]
}

Two defaults that bite quietly

The 7.0 announcement lists new defaults: strict: true, module: esnext, types: [], rootDir as the directory holding the tsconfig.json, and noUncheckedSideEffectImports: true. Two of them change what a working project does without touching any option you wrote.

rootDir decides where emitted files land. Set outDir without rootDir while the sources sit in src, and the compiler stops with TS5011. Both compilers do this, so 6.0 gets you the message before 7.0 does:

tsconfig.json(6,5): error TS5011: The common source directory of 'tsconfig.json' is './src'. The 'rootDir' setting must be explicitly set to this or another path to adjust your output's file layout.

Without that error, the output would have moved from dist/index.js to dist/src/index.js. Anything that runs node dist/index.js would break, and a type check would never notice. Set rootDir explicitly.

types: [] stops ambient packages loading by themselves. In the sample project, import { readFileSync } from "node:fs" fails on both compilers until "types": ["node"] is added, with TS2591 pointing at @types/node. The same applies to the test runner’s globals, so list whatever the project relies on, or use ["*"] to get the old behaviour back while you work out the list.

Pass two: diff the two compilers

With the config clean on 6.0, run both compilers over the same project and compare what they say. The script is short enough to read in full before running it:

#!/usr/bin/env python3
"""Run TypeScript 6 and 7 on the same project and diff their diagnostics."""
import subprocess, sys

def diagnostics(cmd, project):
    out = subprocess.run(["npx", cmd, "-p", project, "--noEmit", "--pretty", "false"],
                         capture_output=True, text=True).stdout
    return {line.strip() for line in out.splitlines() if " error TS" in line}

project = sys.argv[1] if len(sys.argv) > 1 else "tsconfig.json"
old, new = diagnostics("tsc6", project), diagnostics("tsc", project)
for line in sorted(new - old):
    print("only on 7:", line)
for line in sorted(old - new):
    print("only on 6:", line)
print(f"6: {len(old)} errors, 7: {len(new)} errors, identical: {old == new}")
sys.exit(0 if old == new else 1)

On the migrated config:

python3 tsc_diff.py tsconfig.json
6: 0 errors, 7: 0 errors, identical: True

The script compares sets of error lines rather than exit codes or counts, which matters for a reason that only shows up when something fails. On a project with one genuine type error and --noEmit, 6.0.3 exits with code 2 and 7.0.2 exits with code 1, though both print the same TS18048 line. A CI step that tests for a specific exit code, or a wrapper script that treats 2 as “type errors, carry on”, behaves differently after the switch. Check anything that inspects the status.

Run the script against the old config, with ignoreDeprecations still silencing 6.0, and the diff shows exactly what that silencer was hiding (output trimmed):

only on 7: tsconfig.legacy.json(3,15): error TS5108: Option 'target=ES5' has been removed. ...
only on 7: tsconfig.legacy.json(3,64): error TS5108: Option 'moduleResolution=node10' has been removed. ...
only on 7: tsconfig.legacy.json(4,5): error TS5102: Option 'baseUrl' has been removed. ...
only on 6: node_modules/vitest/dist/index.d.ts(17,8): error TS2882: ...
...
6: 8 errors, 7: 3 errors, identical: False

The “only on 6” lines are noise from the ES5 target choking on declaration files in node_modules. They disappear with the config fix. The “only on 7” lines are the work.

Where an agent helps, and where it shouldn’t be trusted

Pass one is mostly mechanical, and a coding agent is a reasonable tool for it with a tight brief. A prompt that works with any of them:

This project is moving from TypeScript 6.0 to 7.0. Remove "ignoreDeprecations"
from tsconfig.json, run `npx tsc6 --noEmit`, and fix each TS5101 and TS5107
by replacing the deprecated option, not by suppressing it. Do not change
"strict", do not add "skipLibCheck", and do not edit any source file. Show me
the tsconfig diff and the new tsc6 output. Stop when tsc6 reports no errors.

The restrictions are the point. An agent that wants a green run will happily set strict to false, add skipLibCheck, or sprinkle // @ts-ignore, and each of those makes the “identical” result in pass two meaningless. Read the tsconfig diff yourself, because the choices of target, module and moduleResolution are decisions about where the code runs, and the compiler can’t know that.

For pass two, give the agent the script and its output and ask only for an explanation of each “only on 7” line. If the list is empty, there’s nothing to ask.

What the diff won’t tell you

An empty diff means the two compilers agree on the errors. It doesn’t prove the emitted JavaScript is the same, and with --noEmit it never looks at it. If the project ships compiled output, run the build with each compiler into separate directories and compare the trees, or at least run the test suite against each.

It also doesn’t cover the JavaScript-file changes the 7.0 notes describe (JSDoc @enum, @class and a bare ? type are no longer special), template-literal inference over non-BMP characters, or the tooling that needs the API. If you use checkJs, expect real “only on 7” lines.

The 7.1 Beta, announced on 20 October 2026, is the release that adds a new programmatic API, split into entry points such as typescript/sync, typescript/async and typescript/ast. It’s a beta, the tools that matter to you may not target it yet, and nothing in the announcement promises the API is frozen. Treat the alias setup above as the stable arrangement for now, and try the beta with npm install -D typescript@beta in a branch.

First 15 minutes

Install @typescript/typescript6 beside TypeScript 7 using the alias above, remove ignoreDeprecations from your tsconfig, and run npx tsc6 --noEmit. Fix what it reports, set rootDir and types explicitly, then run the diff script. Merge when it prints identical: True.