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.
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 subdirectoriessrc/**/*.tsmatches only.tsfiles undersrc**/*.test.tsmatches 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()andmodule.exports. For Node.js without ESM. - ES2015/ES2020/ES2022: Outputs
import/exportstatements. Each version adds support for more features (dynamic import, import.meta, top-level await). - NodeNext: Follows Node.js module resolution. Files with
.mtsextension produce ESM;.ctsproduces CJS. Thepackage.jsontypefield 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, checksmainin package.json, tries.ts,.tsx,.jsextensions. - NodeNext: The modern Node.js algorithm. Respects
exportsin 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
overridekeyword when overriding base class methods - forceConsistentCasingInFileNames: Prevent
import "./File"from matchingfile.tson case-insensitive systems
JavaScript Interop
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"maxNodeModuleJsDepth": 0
}
}
- allowJs: Let TypeScript compile
.jsfiles alongside.ts - checkJs: Apply type checking to
.jsfiles (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
Recommended Configurations
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
NodeNextorBundlerdepending 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.
Related articles
- TypeScript TypeScript Strict Mode and tsconfig Tips
Understand what strict mode actually turns on, which extra flags are worth enabling, and how to adopt strictness incrementally.
- Node.js Node.js TypeScript Setup Tutorial
Set up a clean, modern TypeScript project for Node.js with tsconfig, build scripts, ESM, and a dev loop that does not get in your way.
- TypeScript TypeScript Generics: Advanced Patterns and Constraints
Master advanced TypeScript generics patterns including conditional constraints, recursive types, generic factories, and type-safe builder patterns with examples.
- TypeScript TypeScript Type Guards and Narrowing Explained
Learn how TypeScript type guards and control flow narrowing work, including typeof, instanceof, in, discriminated unions, and custom type predicates.