A TypeScript 7 migration sounds like bumping a version number in package.json, and that is exactly how I treated it at first. Then the first build stops with three errors in tsconfig.json, cdk synth fails with a cryptic message from ts-node, and npm refuses to install typescript-eslint. The compiler itself is faster than ever. It’s the tools around it that slow you down.

This matters to me because Lambda and CDK projects written in TypeScript often run on an old tsconfig for years. So in this article you walk through the whole upgrade once: from checking the configuration to replacing ts-node and keeping ESLint on TypeScript 6 for the time being. Every error message below comes from a real test run with TypeScript 7.0.2.

In short

TypeScript 7 is the new compiler written in Go, and it turns every option that TypeScript 6 deprecated into a hard error. In Lambda and CDK projects that is quick to fix, because esbuild does the building anyway and tsc only checks types. The biggest pitfall is that TypeScript 7.0 ships without a compiler API: ts-node, ts-jest and typescript-eslint need TypeScript 6 alongside it for now.

What you will learn in this article

  • Which tsconfig options TypeScript 7 rejects and what replaces them
  • How a small check script finds the pitfalls before you switch
  • Why ts-node breaks the CDK app and how tsx replaces it
  • How to run TypeScript 6 and 7 side by side for ESLint
  • How to verify that bundle and deployment still work afterwards

What is different in TypeScript 7?

TypeScript 7 is the compiler the TypeScript team has ported to Go over the last few years. Version 7.0 has been stable since July 2026, and the current release is 7.0.2. You still install it from the npm package typescript, and the command is still tsc. If you used the preview package @typescript/native-preview before, you can remove it again.

The obvious gain is speed. In the official announcement, the build time of the VS Code code base drops from about 126 to just under 11 seconds, and for other large projects the factor sits between almost 8 and 9. Native code and parallel type checking make this possible: the new --checkers flag sets how many checker workers run at once (default 4), and --singleThreaded turns that off.

Deprecated options are now errors

For the migration, something else matters more. In March 2026, TypeScript 6.0 deprecated a whole set of options but still accepted them with "ignoreDeprecations": "6.0". TypeScript 7 removes that escape hatch. What used to be a warning in 6.0 now stops the build. At the same time, the new defaults from 6.0 still apply, for example strict: true, types: [] and a rootDir that points to the folder containing tsconfig.json.

Option Status in TypeScript 7 Replacement
moduleResolution: "node" / "node10" removed "bundler" or "nodenext"
baseUrl removed start entries in paths with ./
target: "es5" removed at least ES2015, ES2023 for Lambda
module: "amd", "umd", "system" removed "preserve", "esnext", "nodenext"
esModuleInterop: false no longer allowed delete the line
downlevelIteration, outFile removed delete the line, use a bundler
types not set default [] "types": ["node"]
rootDir not set tsconfig folder "rootDir": "./src"

No compiler API in 7.0

The second big change affects tools that don’t call TypeScript as a command but load it as a library. This so-called compiler API (the programming interface other programs use to parse code and query types) does not exist in 7.0. A new, different API is planned for 7.1, which I already tried out in the article on the TypeScript 7.1 beta. Until it is stable, the TypeScript team ships a compatibility package: @typescript/typescript6 provides the tsc6 command and the old API.

Why Lambda and CDK projects are especially affected

In a typical Serverless project with TypeScript, more tools depend on TypeScript than you notice day to day. The good news: you don’t need the TypeScript compiler to build the Lambda functions at all. NodejsFunction in the AWS CDK and Serverless Framework v4 bundle with esbuild, and esbuild simply strips the types without checking them. Here, tsc is only the type checker.

That makes the tools that need the compiler API the critical ones. Older projects generated with cdk init start the CDK app with ts-node and test with ts-jest. Both load TypeScript as a library. By the way, the current CDK templates have already switched: they use tsx for the app, @swc/jest for tests and TypeScript ~7.0.2.

Tool Needs the compiler API? What you need to do
tsc no, it is the compiler switch to 7, fix tsconfig
esbuild (CDK NodejsFunction, Serverless Framework v4) no nothing
tsx no (uses esbuild) nothing, replaces ts-node
Vitest not for running tests nothing
ts-node yes replace with tsx
ts-jest yes, peer dependency typescript <7 Vitest or @swc/jest, otherwise TypeScript 6
typescript-eslint 8.72 yes, peer dependency <6.1.0 install TypeScript 6 alongside

🎬 Which TypeScript does each tool need after the upgrade? – this animated diagram only runs on my site: watch it on sascha-ms.de

TypeScript 7 migration step by step

The example is a small, made-up bike rental: a Lambda function behind an HTTP API calculates the rental price, a shared module lives in src/shared and is imported through the @shared/* alias. The infrastructure is defined in the AWS CDK, and a serverless.yml shows the same function with the Serverless Framework.

Prerequisites

  • Node.js 22 or newer locally (tested with Node.js 24)
  • a project on TypeScript 5.x or 6.x that currently builds cleanly
  • Git, so you can commit each step separately and roll back if needed
  • for the deployment at the end: AWS credentials and a bootstrapped CDK account (optional)

Step 1: Create a branch and measure the baseline

Before you change anything, write down how long type checking takes today and whether it passes. Otherwise you won’t know afterwards whether an error is new or was already there.

git switch -c chore/typescript-7 npx tsc --version time npx tsc --noEmit 

Step 2: Check the tsconfig automatically

Instead of ticking off the table above by hand, let a small script read tsconfig.json. It has no dependencies, understands comments and trailing commas (the tsconfig is JSONC, meaning JSON with comments) and reports every option that will cause trouble under TypeScript 7. One caveat: the script only checks the file itself. It doesn’t follow an extends to a base configuration, so check that one separately.

// scripts/ts7-check.ts import { readFileSync } from 'node:fs'; export interface Finding { option: string; problem: string; fix: string; } type Options = Record<string, unknown>; /** Removes comments and trailing commas before closing brackets (tsconfig is JSONC). */ export function parseJsonc(text: string): unknown { let out = ''; let inString = false; for (let i = 0; i < text.length; i++) { const ch = text[i]; const next = text[i + 1]; if (inString) { out += ch; if (ch === '\\') { out += next ?? ''; i++; } else if (ch === '"') { inString = false; } continue; } if (ch === '"') { inString = true; out += ch; } else if (ch === '/' && next === '/') { while (i < text.length && text[i] !== '\n') i++; out += '\n'; } else if (ch === '/' && next === '*') { i += 2; while (i < text.length && !(text[i] === '*' && text[i + 1] === '/')) i++; i++; } else { out += ch; } } return JSON.parse(out.replace(/,(\s*[}\]])/g, '$1')); } const lower = (v: unknown) => (typeof v === 'string' ? v.toLowerCase() : v); export function checkCompilerOptions(opts: Options, sourceDirs: string[] = []): Finding[] { const findings: Finding[] = []; const add = (option: string, problem: string, fix: string) => findings.push({ option, problem, fix }); const resolution = lower(opts.moduleResolution); if (resolution === 'node' || resolution === 'node10' || resolution === 'classic') { add('moduleResolution', `"${String(opts.moduleResolution)}" no longer exists`, '"bundler" (with esbuild) or "nodenext" (with tsc output)'); } const mod = lower(opts.module); if (['amd', 'umd', 'system', 'systemjs', 'none'].includes(String(mod))) { add('module', `"${String(opts.module)}" no longer exists`, '"preserve", "esnext" or "nodenext"'); } if (lower(opts.target) === 'es5' || lower(opts.target) === 'es3') { add('target', 'ES5 is no longer a valid target', 'at least "ES2015", for Lambda preferably "ES2023"'); } if ('baseUrl' in opts) { add('baseUrl', 'option was removed', 'write "paths" entries relative, starting with "./"'); } const paths = (opts.paths ?? {}) as Record<string, string[]>; for (const [alias, targets] of Object.entries(paths)) { if (targets.some((t) => !t.startsWith('./') && !t.startsWith('../'))) { add(`paths["${alias}"]`, 'target without a leading "./"', 'e.g. "./src/shared/*" instead of "src/shared/*"'); } } for (const flag of ['esModuleInterop', 'allowSyntheticDefaultImports', 'alwaysStrict']) { if (opts[flag] === false) add(flag, 'false is no longer allowed', 'delete the line'); } if ('downlevelIteration' in opts) add('downlevelIteration', 'option was removed', 'delete the line'); if ('outFile' in opts) add('outFile', 'option was removed', 'use a bundler such as esbuild'); if ('ignoreDeprecations' in opts) { add('ignoreDeprecations', 'no longer helps in TypeScript 7', 'fix the actual causes'); } if (!('types' in opts)) { add('types', 'default is now [] – @types packages are no longer loaded automatically', '"types": ["node"] (plus e.g. "jest")'); } if (!('rootDir' in opts) && !opts.noEmit && sourceDirs.length === 1) { add('rootDir', 'default is now the tsconfig folder', `"rootDir": "./${sourceDirs[0]}"`); } return findings; } function sourceDirsOf(config: { include?: string[] }): string[] { const dirs = (config.include ?? []).map((p) => p.split('/')[0]).filter((d) => d && !d.includes('*')); return [...new Set(dirs)]; } if (process.argv[1]?.endsWith('ts7-check.ts')) { const file = process.argv[2] ?? 'tsconfig.json'; const config = parseJsonc(readFileSync(file, 'utf8')) as { compilerOptions?: Options; include?: string[] }; const findings = checkCompilerOptions(config.compilerOptions ?? {}, sourceDirsOf(config)); if (findings.length === 0) { console.log(`${file}: no known TypeScript 7 pitfalls found.`); } else { console.log(`${file}: ${findings.length} item(s) to fix before moving to TypeScript 7:`); for (const f of findings) console.log(`- ${f.option}: ${f.problem} → ${f.fix}`); process.exitCode = 1; } } 

You run it with tsx, which you’ll need in a moment anyway. For the bike rental’s old configuration, the result looks like this:

npx tsx scripts/ts7-check.ts tsconfig.json 
tsconfig.json: 4 item(s) to fix before moving to TypeScript 7: - moduleResolution: "node" no longer exists → "bundler" (with esbuild) or "nodenext" (with tsc output) - baseUrl: option was removed → write "paths" entries relative, starting with "./" - paths["@shared/*"]: target without a leading "./" → e.g. "./src/shared/*" instead of "src/shared/*" - types: default is now [] – @types packages are no longer loaded automatically → "types": ["node"] (plus e.g. "jest") 

Step 3: Install TypeScript 7 and look at the first run

Now install TypeScript 7 and run the type check once. Expect errors; that’s the point of the exercise.

npm install --save-dev typescript@^7.0.2 npx tsc --noEmit 
tsconfig.json(6,25): error TS5108: Option 'moduleResolution=node10' has been removed. Please remove it from your configuration. tsconfig.json(8,5): error TS5102: Option 'baseUrl' has been removed. Please remove it from your configuration. Use '"paths": {"*": ["./*"]}' instead. tsconfig.json(9,30): error TS5090: Non-relative paths are not allowed. Did you forget a leading './'? 

Notice that TypeScript 7 only complains about the configuration at first. You’ll see errors in your code once the tsconfig is valid again. Under TypeScript 6, the same messages were still TS5107 and TS5101, and ignoreDeprecations could silence them.

Step 4: Fix modules and paths

For Lambda code that esbuild bundles, "moduleResolution": "bundler" together with "module": "preserve" is the right choice. This combination mirrors how bundlers resolve imports and allows imports without file extensions. If tsc emits JavaScript for Node.js itself, use "nodenext" for both options instead. baseUrl goes away, and the targets in paths start with ./. Here is the old configuration:

{ // Typical configuration from the TypeScript 5 days "compilerOptions": { "target": "ES2020", "module": "commonjs", "moduleResolution": "node", "lib": ["es2020"], "baseUrl": ".", "paths": { "@shared/*": ["src/shared/*"] }, "outDir": "dist", "strict": true, "esModuleInterop": true, "skipLibCheck": true, }, "include": ["src/**/*.ts", "test/**/*.ts"] } 

And here is the new one. Since esbuild does the building, it also sets noEmit: tsc no longer writes files and only checks.

{ "compilerOptions": { "target": "ES2023", "lib": ["ES2023"], "module": "preserve", "moduleResolution": "bundler", "types": ["node"], "paths": { "@shared/*": ["./src/shared/*"] }, "strict": true, "noEmit": true, "isolatedModules": true, "skipLibCheck": true }, "include": ["bin", "lib", "src", "scripts", "test"] } 

Step 5: Bring back the Node types

If you leave out types, TypeScript no longer loads @types packages automatically since 6.0. In the Lambda handler you notice it right away at process.env:

src/rental.ts(5,17): error TS2591: Cannot find name 'process'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig. 

The message already contains the fix: add "types": ["node"] to compilerOptions. If you use Jest with global functions like describe, add "jest" as well. The types from @types/aws-lambda need no entry, because you pull them in explicitly with import type.

Step 6: Set rootDir when tsc writes files itself

This step only affects you if tsc emits JavaScript, for example for a library or a container. TypeScript used to guess the common source folder. Now rootDir defaults to the folder containing tsconfig.json, and TypeScript 7 stops instead of quietly rearranging the output:

tsconfig.json(10,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. Visit https://aka.ms/ts6 for migration information. 

With "rootDir": "./src", src/rental.ts ends up in dist/rental.js again, not in dist/src/rental.js. That matters, because a Lambda handler path like dist/rental.handler would otherwise point nowhere.

Step 7: Replace ts-node with tsx in the CDK app

Now comes the part that costs the most time in older CDK projects. There, cdk.json typically starts the app with npx ts-node --prefer-ts-exts bin/app.ts. With TypeScript 7 in the project, it ends like this:

TypeError: Cannot read properties of undefined (reading 'fileExists') at readConfig (node_modules/ts-node/dist/configuration.js:91:33) 

ts-node accesses ts.sys from the compiler API, and that API is gone. tsx, on the other hand, transpiles TypeScript with esbuild and doesn’t need TypeScript at all. The change is a single line:

npm uninstall ts-node npm install --save-dev tsx 
{ "app": "npx tsx bin/app.ts", "watch": { "include": ["bin/**", "lib/**", "src/**"], "exclude": ["cdk.out", "node_modules", "test"] } } 

Because tsx doesn’t check types, npx tsc --noEmit now belongs in your CI pipeline for good. The current CDK template handles this by running npx tsc first and tsx second in cdk.json. That adds a little time to every cdk synth, but it prevents a type error from slipping into a deployment unnoticed.

Step 8: Tests without ts-jest

According to its package metadata, ts-jest requires TypeScript below version 7. You have two options. Either you switch to Vitest, which transpiles TypeScript itself and doesn’t need the compiler API. I show what that looks like for Lambda handlers in Lambda unit tests with Vitest. If you already have Jest tests and want to move them all, the step-by-step guide in Migrate Jest to Vitest walks you through it, check script included. Or you stay with Jest and only swap the transformer for @swc/jest, just like the new CDK template does. In the example project, handler logic, check script and CDK stack all run on Vitest:

// test/rental-stack.test.ts import { App } from 'aws-cdk-lib'; import { Template } from 'aws-cdk-lib/assertions'; import { describe, it } from 'vitest'; import { RentalStack } from '../lib/rental-stack'; describe('RentalStack', () => { it('creates a Node.js 24 function on ARM', () => { const template = Template.fromStack(new RentalStack(new App(), 'TestStack')); template.hasResourceProperties('AWS::Lambda::Function', { Runtime: 'nodejs24.x', Architectures: ['arm64'], }); }); }); 

Step 9: Run ESLint with TypeScript 6 alongside

typescript-eslint is the tool you can’t easily swap out. Version 8.72 requires TypeScript from 4.8.4 and below 6.1.0. If you only install TypeScript 7, npm refuses:

npm error code ERESOLVE npm error Could not resolve dependency: npm error peer typescript@">=4.8.4 <6.1.0" from typescript-eslint@8.72.2 

The way out is two npm aliases. Under the name typescript you install the compatibility package with TypeScript 6, and under @typescript/native the new compiler. typescript-eslint finds its API this way, and npx tsc still starts TypeScript 7, because only the native package provides a command called tsc. In the terminal, the compatibility package is called tsc6.

npm install --save-dev "@typescript/native@npm:typescript@^7.0.2" "typescript@npm:@typescript/typescript6@^6.0.2" npx tsc --version npx tsc6 --version 
Version 7.0.2 Version 6.0.3 

The ESLint configuration stays as it was. In the example, only one rule is turned off, because Lambda handlers are async on purpose:

// eslint.config.mjs import tseslint from 'typescript-eslint'; export default tseslint.config( Source:https://dev.to/sascha-ms/typescript-7-migration-lambda-and-cdk-projects-3ho3

Related posts