Overview

Set module and moduleResolution as a pair, set types explicitly, and stop using options that TypeScript 6 deprecated and 7 no longer supports. Use NodeNext for Node ESM, a shared base config split per package, and project references for monorepos. strict and the extra safety flags are in typescript-strict-mode.

Know what TypeScript 6 changed

  • strict is true by default; module defaults to esnext; target floats to the newest ES version (es2025 today).
  • types defaults to [], so ambient globals no longer load from node_modules/@types. Add "types": ["node"] (and "vite/client" or similar) or process and Buffer become undeclared.
  • rootDir defaults to the folder holding tsconfig.json. Set "rootDir": "./src" if your sources sit deeper and you emit.
  • noUncheckedSideEffectImports defaults to true, and libReplacement to false.

Remove options that 7.0 does not support

Deprecated in 6.0 and unsupported in 7.0: target: es5, downlevelIteration, moduleResolution: node (also called node10), classic, module values amd, umd, systemjs, and none, baseUrl, outFile, esModuleInterop: false, allowSyntheticDefaultImports: false, and alwaysStrict: false. "ignoreDeprecations": "6.0" silences the 6.0 errors but only buys time; 7.0 does not support these options at all.

Replace baseUrl with relative paths entries ("@app/*": ["./src/app/*"]). Interop behavior is always on, so import express from "express" works against CommonJS and the explicit esModuleInterop: true is redundant. If a 6 to 7 upgrade changes diagnostics, --stableTypeOrdering on 6.0 makes its type ordering match 7.0 for diffing; it is a diagnostic aid, not a permanent setting.

Use this baseline for a bundled app

For Vite, Next.js, or Astro the bundler resolves and emits, so tsc only checks.

{
  "compilerOptions": {
    "target": "ES2023",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "lib": ["ES2023", "DOM", "DOM.Iterable"],
    "types": [],
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitOverride": true,
    "verbatimModuleSyntax": true,
    "skipLibCheck": true,
    "noEmit": true
  },
  "include": ["src"]
}

target is the newest syntax your oldest runtime runs; it never polyfills APIs. skipLibCheck skips .d.ts files, which speeds checking at the cost of missing wrong dependency types. verbatimModuleSyntax requires import type for type-only imports and implies isolatedModules, so do not add both.

Use NodeNext for Node ESM

Node ESM code needs "module": "NodeNext" and "moduleResolution": "NodeNext" together, with explicit file extensions in relative imports and "types": ["node"].

To run .ts through Node’s type stripping, use the options Node’s documentation recommends:

{
  "compilerOptions": {
    "noEmit": true,
    "target": "esnext",
    "module": "nodenext",
    "rewriteRelativeImportExtensions": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true
  }
}

Share a base config; split per package

Put strictness flags in tsconfig.base.json and let each package extends it and choose its own module, outDir, and include. Keep tsconfig.test.json extending the package config when tests need different include or types.

Use project references for monorepos

References make tsc --build compile packages in dependency order and skip unchanged ones. Each referenced package needs composite: true.

{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": { "composite": true, "rootDir": "src", "outDir": "dist" },
  "references": [{ "path": "../shared" }, { "path": "../db" }]
}

See monorepo for the wider layout. Case-insensitive filesystems hide wrong import casing, so keep forceConsistentCasingInFileNames: true on to catch it before Linux CI does.