---
title: "TypeScript: tsconfig.json"
slug: "typescript-tsconfig"
category: "coding"
tags: ["coding", "typescript", "tsconfig", "compiler", "monorepo"]
status: "stable"
last_updated: 2026-08-14
summary: "The compilerOptions worth setting, per-project vs base config, and project references for monorepos."
related: ["[[coding/typescript]]", "[[coding/typescript-strict-mode]]", "[[coding/typescript-runtime-validation]]", "[[coding/typescript-utility-types]]", "[[file-organization/monorepo]]"]
---

## 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 [[coding/typescript-strict-mode]] for the `strict` rationale; this page covers the rest of the surface.

## The baseline config

For a typical TypeScript 5.9+ project (6.0 is current) that targets Node 24 (Active LTS; Node 22 is in Maintenance LTS) or a modern bundler, this is the starting point.

```json
{
  "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 24 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 24, `"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 [[coding/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.

```ts
// 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 [[coding/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.

```json
{
  "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.

```json
// 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 [[file-organization/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.

```json
// 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 [[coding/testing]] for the wider testing rules.

## Related

- [[coding/typescript]]
- [[coding/typescript-strict-mode]]
- [[coding/typescript-runtime-validation]]
- [[coding/typescript-utility-types]]
- [[file-organization/monorepo]]
