Overview

tsconfig.json controls every type check that hits your codebase. Most teams copy a config from a starter and never touch it; that is the wrong default. Pick the options that match the runtime and build pipeline, set them explicitly, and review them when the toolchain changes. Read typescript-strict-mode for the strict rationale; this page covers the rest of the surface.

The baseline config

For a typical TypeScript 5.5+ project that targets Node 22 or a modern bundler, this is the starting point.

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

Every option here is load-bearing. The rest of this page explains which ones to change for which target.

Set target to what the runtime supports

target controls the JS syntax that tsc (or a bundler honoring this option) downlevels to. Set it to the lowest version your runtime supports, not the highest.

  • Modern bundlers (Vite, esbuild, swc) targeting evergreen browsers: ES2022 or higher.
  • Node 22 server code: ES2023.
  • A package published to npm for broad consumption: ES2020 so older Node 18 consumers can read it without further downlevel.

target does not polyfill runtime APIs; it only controls syntax. For runtime polyfills, configure the bundler.

Pick module and moduleResolution together

module controls the emitted module format; moduleResolution controls how imports are looked up. They must agree.

  • Bundled apps (Vite, Next.js, Astro): "module": "ESNext" and "moduleResolution": "Bundler". The bundler resolves; tsc only type-checks.
  • Modern Node ESM (Node 22, "type": "module"): "module": "NodeNext" and "moduleResolution": "NodeNext". Imports need explicit .js extensions.
  • Library targeting both: "module": "NodeNext" with a dual-publish build step.

"moduleResolution": "Node" (the old default) is deprecated; switch to Bundler or NodeNext explicitly. See typescript for the ESM-only rule.

Set esModuleInterop and skipLibCheck to true

These two pull their weight on every project.

  • esModuleInterop: true lets import express from "express" work even when the upstream module is CommonJS. Without it, you write import * as express from "express" and the runtime breaks.
  • skipLibCheck: true skips type-checking inside .d.ts files in node_modules. The win is a 5x to 10x speedup on cold tsc runs. The cost is that a bug in a dependency’s types can sneak through; the cost is worth paying.

Both are in the baseline above. Set them and forget them.

Use verbatimModuleSyntax to keep imports honest

verbatimModuleSyntax: true forces you to mark type-only imports with import type. The runtime import then matches what tsc emits exactly; no surprise erasure.

// With verbatimModuleSyntax: true
import type { User } from "./types"
import { loadUser } from "./users"
 
// Without it, this might compile but emit weirdly:
// import { User, loadUser } from "./users"

This flag also catches the bug where you import a type from a file that has side effects; without import type, the file is loaded at runtime for no reason. See typescript-types-vs-interfaces for the type-vs-runtime distinction.

Turn on forceConsistentCasingInFileNames

forceConsistentCasingInFileNames: true makes the compiler reject imports that differ from the filesystem casing. Without it, import { x } from "./Foo" compiles on macOS (case-insensitive) and breaks on Linux CI (case-sensitive).

This is a zero-cost setting that prevents a class of “works on my machine” bugs. Always on.

Use isolatedModules when a bundler emits

isolatedModules: true makes tsc reject syntax that bundlers like esbuild and swc cannot compile file-by-file (const enum, certain re-exports). The flag is the contract between tsc for type-checking and a bundler for emit.

{
  "compilerOptions": {
    "isolatedModules": true,
    "noEmit": true
  }
}

If a bundler is in the build chain, set isolatedModules: true and noEmit: true. tsc --noEmit becomes the type check; the bundler handles output.

Use a base config plus per-package overrides

For anything larger than a single package, use extends to share a base.

// tsconfig.base.json
{
  "compilerOptions": {
    "target": "ES2022",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true
  }
}
 
// packages/api/tsconfig.json
{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist"
  },
  "include": ["src"]
}

Each package picks its own module, outDir, and include. The base owns the strictness flags. See monorepo for the wider layout rules.

Use project references for monorepos

In a monorepo where packages depend on each other, references tell tsc to build them in order and reuse intermediate results.

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

tsc --build (note --build, not the bare command) walks the references, builds in dependency order, and caches. The first build is the same speed as without references; every subsequent build skips packages whose inputs did not change. For large monorepos this is the difference between a 90-second typecheck and a 5-second one.

Keep test config separate

Tests need different include paths, sometimes different lib entries, and looser rules on unused vars. Put them in tsconfig.test.json that extends the main config; do not pollute the production config with test-only relaxations. See testing for the wider testing rules.