Skip to content
Codeloom
TypeScript

tsconfig.json Deep Dive: Every Option Explained

A comprehensive guide to tsconfig.json covering compiler options, module resolution, path aliases, project references, and recommended configurations.

·8 min read · By Codeloom
Intermediate 15 min read

What you'll learn

  • The structure and inheritance of tsconfig.json
  • Key compiler options and what they do
  • Module resolution strategies
  • Path aliases, project references, and recommended configs

Prerequisites

  • Basic TypeScript knowledge
  • Familiarity with Node.js projects
  • Understanding of module systems (CommonJS, ESM)

The Structure of tsconfig.json

Every TypeScript project starts with a tsconfig.json file. It tells the compiler which files to include, how to compile them, and what checks to enforce. The top-level structure has several sections:

{
  "compilerOptions": { },
  "include": [],
  "exclude": [],
  "files": [],
  "extends": "",
  "references": []
}

Let us examine each section and the most important options within compilerOptions.

File Inclusion

include and exclude

These fields use glob patterns to determine which files TypeScript should compile:

{
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}

The include field defaults to ["**/*"] if omitted, meaning everything in the project directory. The exclude field defaults to ["node_modules", "bower_components", "jspm_packages"] plus the outDir if specified.

Common patterns:

  • **/* matches all files in all subdirectories
  • src/**/*.ts matches only .ts files under src
  • **/*.test.ts matches test files at any depth

files

For explicit control, files lists individual file paths. Unlike include, it does not support globs:

{
  "files": ["src/index.ts", "src/types.ts"]
}

This is rarely used in real projects because maintaining a manual list is tedious.

extends: Config Inheritance

You can extend another config file, overriding specific options:

{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "outDir": "./dist"
  },
  "include": ["src/**/*"]
}

The base config provides shared settings, and each project-specific config overrides what it needs. This is common in monorepos:

tsconfig.base.json          # Shared strict settings
packages/
  api/tsconfig.json          # extends ../../tsconfig.base.json
  web/tsconfig.json          # extends ../../tsconfig.base.json

You can also extend from npm packages:

{
  "extends": "@tsconfig/node20/tsconfig.json"
}

The @tsconfig scope on npm provides community-maintained base configs for common environments.

Target and Module

target

The target option sets the JavaScript version for the output:

{
  "compilerOptions": {
    "target": "ES2022"
  }
}

Common values:

  • ES5: Maximum compatibility. Async/await, classes, and arrow functions are downcompiled.
  • ES2015/ES6: Classes and arrow functions kept. Async/await still downcompiled.
  • ES2020: Supports optional chaining, nullish coalescing, BigInt.
  • ES2022: Supports top-level await, class fields, at() method.
  • ESNext: Latest features. Moves with each TypeScript release.

The target affects which lib types are included automatically. Setting target: "ES2022" implicitly adds lib: ["ES2022"].

module

The module option determines the module system of the output:

{
  "compilerOptions": {
    "module": "NodeNext"
  }
}

Key values:

  • CommonJS: Outputs require() and module.exports. For Node.js without ESM.
  • ES2015/ES2020/ES2022: Outputs import/export statements. Each version adds support for more features (dynamic import, import.meta, top-level await).
  • NodeNext: Follows Node.js module resolution. Files with .mts extension produce ESM; .cts produces CJS. The package.json type field determines the default.
  • Preserve: Keeps the import/export syntax exactly as written. Useful when a bundler handles modules.

moduleResolution

This controls how TypeScript finds modules when you write import:

{
  "compilerOptions": {
    "moduleResolution": "NodeNext"
  }
}
  • node (or node10): The classic Node.js algorithm. Looks in node_modules, checks main in package.json, tries .ts, .tsx, .js extensions.
  • NodeNext: The modern Node.js algorithm. Respects exports in package.json, requires file extensions in relative imports, handles .mts/.cts.
  • Bundler: Like NodeNext but does not require file extensions. Use this with webpack, Vite, esbuild, or other bundlers.

For new projects in 2026, use NodeNext for Node.js applications and Bundler for frontend projects.

Output Options

outDir and rootDir

{
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src"
  }
}

outDir is where compiled files go. rootDir determines the directory structure inside outDir. If your source is in src/utils/math.ts and rootDir is src, the output is dist/utils/math.js.

declaration and declarationMap

{
  "compilerOptions": {
    "declaration": true,
    "declarationMap": true,
    "declarationDir": "./dist/types"
  }
}

declaration generates .d.ts files alongside .js output. declarationMap creates .d.ts.map files for “Go to Definition” to navigate to source .ts files instead of .d.ts. Essential for libraries.

sourceMap

{
  "compilerOptions": {
    "sourceMap": true
  }
}

Generates .js.map files that let debuggers map compiled JavaScript back to TypeScript source. Use inlineSourceMap to embed the map in the JS file (useful for some debugging setups).

emitDeclarationOnly

{
  "compilerOptions": {
    "emitDeclarationOnly": true
  }
}

Only emits .d.ts files, no JavaScript. Use this when a bundler (like esbuild or SWC) handles the JavaScript compilation but you still want TypeScript to generate type definitions.

Path Aliases

Path aliases let you use clean import paths instead of relative paths:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
      "@components/*": ["./src/components/*"],
      "@utils/*": ["./src/utils/*"],
      "@types/*": ["./src/types/*"]
    }
  }
}

Now you can write:

import { Button } from "@components/Button";
import { formatDate } from "@utils/date";

Instead of:

import { Button } from "../../../components/Button";
import { formatDate } from "../../utils/date";

Important: paths only tells TypeScript how to resolve types. Your build tool or runtime needs its own configuration to resolve these paths. For example, Vite uses resolve.alias, and Node.js can use the --import flag with a loader or imports in package.json.

Strict Mode Options

These are covered in detail in our strict mode guide, but here is the summary:

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true
  }
}

The strict flag enables: strictNullChecks, noImplicitAny, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, useUnknownInCatchVariables, and alwaysStrict.

The two additional flags (noUncheckedIndexedAccess and exactOptionalPropertyTypes) are not part of strict but are recommended for maximum safety.

Linting-Style Options

These options catch potential mistakes without changing semantics:

{
  "compilerOptions": {
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitReturns": true,
    "noImplicitOverride": true,
    "forceConsistentCasingInFileNames": true
  }
}
  • noUnusedLocals: Error on declared but unused variables
  • noUnusedParameters: Error on unused function parameters (prefix with _ to suppress)
  • noFallthroughCasesInSwitch: Error on switch cases that fall through without break
  • noImplicitReturns: Error when not all code paths return a value
  • noImplicitOverride: Require the override keyword when overriding base class methods
  • forceConsistentCasingInFileNames: Prevent import "./File" from matching file.ts on case-insensitive systems

JavaScript Interop

{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": true,
    "maxNodeModuleJsDepth": 0
  }
}
  • allowJs: Let TypeScript compile .js files alongside .ts
  • checkJs: Apply type checking to .js files (uses JSDoc comments for types)
  • maxNodeModuleJsDepth: How deep to look into node_modules for JS type inference (default 0)

These are useful during gradual TypeScript adoption.

Lib: Runtime Type Definitions

The lib option specifies which built-in type definitions to include:

{
  "compilerOptions": {
    "lib": ["ES2022", "DOM", "DOM.Iterable"]
  }
}

Common lib values:

  • ES2015-ES2023: ECMAScript features for that year
  • DOM: Browser APIs (window, document, fetch, etc.)
  • DOM.Iterable: Iterable DOM types (NodeList, etc.)
  • WebWorker: Web Worker APIs
  • ESNext: Bleeding-edge proposals

For a Node.js backend, omit DOM:

{
  "compilerOptions": {
    "lib": ["ES2022"]
  }
}

Then install @types/node for Node.js-specific APIs.

Project References

For monorepos, project references let you split a codebase into smaller TypeScript projects that build independently:

{
  "references": [
    { "path": "./packages/common" },
    { "path": "./packages/api" },
    { "path": "./packages/web" }
  ]
}

Each referenced project must have composite: true:

{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "declarationMap": true,
    "outDir": "./dist"
  }
}

Benefits:

  • Faster incremental builds (only rebuild changed projects)
  • Enforce module boundaries between packages
  • Each project can have different compiler options

Build with tsc --build (or tsc -b):

tsc -b --watch

Node.js Backend (2026)

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "sourceMap": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

React/Vite Frontend

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "jsx": "react-jsx",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    },
    "noEmit": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

Note noEmit: true because Vite handles compilation. TypeScript is used only for type checking.

Library Package

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "skipLibCheck": true
  },
  "include": ["src/**/*"]
}

Lower target for wider compatibility. Declarations and maps for consumers.

Wrapping Up

The tsconfig.json file controls every aspect of TypeScript compilation. The most critical decisions are:

  • strict: true for maximum type safety
  • target and module to match your runtime environment
  • moduleResolution set to NodeNext or Bundler depending on your toolchain
  • paths for clean import aliases
  • declaration when building a library

Start with a community base config (@tsconfig/node20, @tsconfig/strictest) and customize from there. When something does not compile the way you expect, check these options first — the answer is usually in the config.