tsconfig.json — Configuring TypeScript
Every serious TypeScript project has a tsconfig.json file at the root. It tells the
TypeScript compiler which files to include, which JavaScript version to target, and
how strictly to check your code. Instead of passing a dozen CLI flags every time you
run tsc, you put them all in one file and commit it to source control.
Generating tsconfig.json
The easiest way to create a tsconfig.json is to let tsc generate one:
npx tsc --init
Created a new tsconfig.json with: target: es2016 module: commonjs strict: true esModuleInterop: true skipLibCheck: true forceConsistentCasingInFileNames: true
The generated file contains every available option, with most commented out. The active defaults are a reasonable starting point — you rarely need to change much.
Basic Structure
A tsconfig.json has three main sections:
{
"compilerOptions": {
// All the type-checking and emit settings
},
"include": ["src/**/*"], // which files to compile
"exclude": ["node_modules"], // which files to skip
"files": ["src/index.ts"] // explicit file list (optional)
}If you omit include, TypeScript includes all .ts, .tsx, and .d.ts files in
the project directory and all subdirectories, except those in node_modules.
Production-Ready tsconfig.json Example
Here is a well-annotated tsconfig suitable for a modern Node.js project:
{
"compilerOptions": {
// ── Output ────────────────────────────────────────────────
"target": "ES2022", // emit modern JS
"module": "node16", // Node.js ESM/CJS dual-mode
"outDir": "dist", // compiled JS goes here
"rootDir": "src", // source root
"declaration": true, // emit .d.ts files
"sourceMap": true, // emit .js.map files
// ── Strict checks ─────────────────────────────────────────
"strict": true, // enables all strict flags
"noUnusedLocals": true, // error on unused variables
"noUnusedParameters": true, // error on unused params
"noImplicitReturns": true, // all code paths must return
"noFallthroughCasesInSwitch": true, // no fallthrough in switch
// ── Module resolution ─────────────────────────────────────
"moduleResolution": "node16", // matches --module node16
"esModuleInterop": true, // default imports from CJS
"resolveJsonModule": true, // can import .json files
"baseUrl": ".", // base for path aliases
"paths": {
"@utils/*": ["src/utils/*"],
"@models/*": ["src/models/*"]
},
// ── Quality ───────────────────────────────────────────────
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true // skip checking .d.ts in node_modules
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}Key compilerOptions Explained
Option | Default | What it controls |
|---|---|---|
target | ES3 | Which JavaScript version to emit |
module | CommonJS (when target ES3/ES5) | Module system in emitted code |
strict | false | Master switch for all strict checks |
outDir | (same as source) | Output directory for compiled files |
rootDir | (inferred) | Root of source files |
baseUrl | undefined | Base directory for non-relative module resolution |
paths | undefined | Path alias mappings |
lib | (derived from target) | Built-in API type declarations to include |
esModuleInterop | false | Allow default imports from CommonJS modules |
skipLibCheck | false | Skip type checking of declaration files in node_modules |
declaration | false | Emit .d.ts files alongside .js output |
sourceMap | false | Emit source maps for debugging |
noUnusedLocals | false | Error on declared but unused local variables |
noUnusedParameters | false | Error on declared but unused function parameters |
noImplicitReturns | false | Error when not all code paths return a value |
forceConsistentCasingInFileNames | false | Error on imports with inconsistent casing |
target — JavaScript Output Version
target controls which JavaScript features tsc uses in the emitted code. Set it
as high as your runtime supports:
Target | Use when |
|---|---|
ES3 | Supporting very old browsers (IE6-era). Almost never needed today. |
ES5 | Supporting IE11. Arrow functions and template literals are downleveled. |
ES2015 / ES6 | Widely supported. Keeps arrow functions, classes, template literals. |
ES2020 | Node.js 12+ and modern browsers. Keeps optional chaining, nullish coalescing. |
ES2022 | Node.js 16+ and modern browsers. Keeps top-level await, class fields. |
ESNext | Latest Node.js / modern browsers. Emits the most modern code. |
"target": "ES2022". For frontend apps using a bundler (Vite, webpack), use "target": "ES2020" and let the bundler handle browser compatibility.strict Mode — What It Enables
Setting "strict": true is a single switch that enables six individual checks. You
can also enable them individually — useful when migrating a JavaScript codebase
gradually:
{
"compilerOptions": {
// These are all turned on by "strict": true
"strictNullChecks": true, // null/undefined are not assignable to other types
"noImplicitAny": true, // error on implicit 'any' types
"strictFunctionTypes": true, // stricter checking of function types
"strictBindCallApply": true, // stricter bind/call/apply checking
"strictPropertyInitialization": true, // class properties must be initialized
"noImplicitThis": true, // 'this' must have a known type
"alwaysStrict": true // emit "use strict" in all files
}
}"strict" to silence errors on a new project — fix the errors instead. On legacy projects being migrated, it is acceptable to enable strict flags one at a time.lib — Built-in Type Declarations
The lib option controls which built-in APIs TypeScript knows about. By default it is
derived from target — if you target ES2020, you automatically get ES2020 lib types.
You need to set lib explicitly when:
- Running in the browser (to get
DOMtypes) - Using features from a newer lib than your target
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"]
}
}Common lib values:
"ES2020"— ES2020 globals (Promise, Map, Set, etc.)"DOM"— browser APIs (document, window, fetch, etc.)"DOM.Iterable"— makes DOM collections iterable (NodeList, etc.)"ESNext"— latest JavaScript globals
DOMfrom lib and install @types/node instead. Mixing both can cause confusing type conflicts.baseUrl and paths — Module Aliases
Without aliases, deep imports look like this:
import { formatDate } from '../../../utils/date';
import { UserModel } from '../../../models/user';With baseUrl and paths you can write:
import { formatDate } from '@utils/date';
import { UserModel } from '@models/user';Configure this in tsconfig.json:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@utils/*": ["src/utils/*"],
"@models/*": ["src/models/*"],
"@components/*": ["src/components/*"]
}
}
}tsconfig.json path aliases are for TypeScript's type checker only. At runtime, Node.js does not know about them. You also need to configure the same aliases in your bundler (webpack/Vite) or use a package liketsconfig-paths for ts-node.include, exclude, and files
These three arrays control which files TypeScript processes:
{
"include": [
"src/**/*" // all .ts/.tsx/.d.ts under src/
],
"exclude": [
"node_modules", // always exclude this
"dist", // don't re-compile compiled output
"**/*.test.ts", // skip test files (if using a separate test tsconfig)
"**/*.spec.ts"
],
"files": [
"src/index.ts" // explicit list — use instead of include for small projects
]
}Glob patterns in include / exclude:
**/*— all files recursivelysrc/**/*.ts— only .ts files under src??— any two characters
node_modules is always excluded even if you do not list it.
extends — Sharing Config
Large projects or monorepos often have a base config that specific configs extend:
// tsconfig.base.json
{
"compilerOptions": {
"target": "ES2022",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}// tsconfig.json (extends the base)
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"module": "commonjs",
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*"]
}// tsconfig.test.json (for tests)
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"module": "commonjs",
"types": ["jest", "node"]
},
"include": ["src/**/*", "tests/**/*"]
}You can also extend from community base configs. The @tsconfig/node20 package
provides a well-maintained config for Node.js 20 projects:
npm install --save-dev @tsconfig/node20
{
"extends": "@tsconfig/node20/tsconfig.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*"]
}Project References
In a monorepo with multiple TypeScript packages, project references let tsc
compile them in the correct order and cache results:
// Root tsconfig.json
{
"references": [
{ "path": "./packages/shared" },
{ "path": "./packages/server" },
{ "path": "./packages/client" }
]
}// packages/server/tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"composite": true // required for project references
},
"references": [
{ "path": "../shared" } // server depends on shared
]
}# Build all packages in dependency order npx tsc --build # Build and watch all packages npx tsc --build --watch
noUnusedLocals and noUnusedParameters
These two flags catch dead code at compile time — a common source of bugs:
// With noUnusedLocals: true
function processUser(id: number) {
const unusedVar = 'hello'; // error: 'unusedVar' is declared but never read
return id * 2;
}
// With noUnusedParameters: true
function add(a: number, b: number, extra: number): number { // error: 'extra' declared but never read
return a + b;
}To intentionally ignore a parameter, prefix it with _:
// Prefix with _ to suppress "unused parameter" error
function add(a: number, b: number, _extra: number): number {
return a + b; // _extra is intentionally ignored
}noImplicitReturns
This flag ensures every code path in a function that returns a value actually returns:
// With noImplicitReturns: true — this errors
function getLabel(code: number): string {
if (code === 1) return 'one';
if (code === 2) return 'two';
// error: Not all code paths return a value
}
// Fix: add a default return
function getLabel(code: number): string {
if (code === 1) return 'one';
if (code === 2) return 'two';
return 'unknown'; // explicit fallthrough
}esModuleInterop
Many npm packages are written as CommonJS modules. Without esModuleInterop, you
cannot import them with the natural default import syntax:
// Without esModuleInterop: true — you must write import * as express from 'express'; // With esModuleInterop: true — natural syntax works import express from 'express'; import fs from 'fs'; import path from 'path';
"esModuleInterop": true in new projects. It is required by many popular libraries. The tsc --init defaults already enable it.Common tsconfig Mistakes
Setting "strict": false — always use strict mode on new code
Forgetting "include" — TypeScript may pick up unwanted files
Not setting "outDir" — compiled .js lands next to .ts files, polluting your source
Path aliases in tsconfig not mirrored in the bundler — imports work in the editor but fail at runtime
Using "files" instead of "include" for large projects — "files" does not support globs
Setting "skipLibCheck": false without fixing the underlying .d.ts errors — just use true
Frontend tsconfig (Vite / React)
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler", // Vite-aware resolution
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"jsx": "react-jsx", // React 17+ JSX transform
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true // Vite handles emit; tsc just type-checks
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}Node.js tsconfig (Express / API)
{
"compilerOptions": {
"target": "ES2022",
"module": "node16",
"moduleResolution": "node16",
"lib": ["ES2022"],
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"sourceMap": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}target, module, strict,outDir, and rootDir. Start with tsc --init, enable strict, and add options as your project needs them.