TypeScripttsconfig.json

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:

Bash
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:

JSON
{
  "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:

JSON
{
  "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.

Tip
For Node.js 18+ backends, use "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:

JSON
{
  "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
  }
}
Warning
Never turn off "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 DOM types)
  • Using features from a newer lib than your target

JSON
{
  "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
Note
For Node.js projects without a browser environment, omit 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:

TS
import { formatDate } from '../../../utils/date';
import { UserModel } from '../../../models/user';

With baseUrl and paths you can write:

TS
import { formatDate } from '@utils/date';
import { UserModel } from '@models/user';

Configure this in tsconfig.json:

JSON
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@utils/*": ["src/utils/*"],
      "@models/*": ["src/models/*"],
      "@components/*": ["src/components/*"]
    }
  }
}
Warning
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:

JSON
{
  "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 recursively
  • src/**/*.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:

JSON
// tsconfig.base.json
{
  "compilerOptions": {
    "target": "ES2022",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}

JSON
// tsconfig.json (extends the base)
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "module": "commonjs",
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src/**/*"]
}

JSON
// 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:

Bash
npm install --save-dev @tsconfig/node20

JSON
{
  "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:

JSON
// Root tsconfig.json
{
  "references": [
    { "path": "./packages/shared" },
    { "path": "./packages/server" },
    { "path": "./packages/client" }
  ]
}

JSON
// packages/server/tsconfig.json
{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "outDir": "./dist",
    "composite": true    // required for project references
  },
  "references": [
    { "path": "../shared" }  // server depends on shared
  ]
}

Bash
# 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:

TS
// 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 _:

TS
// 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:

TS
// 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:

TS
// 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';
Tip
Always set "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)

JSON
{
  "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)

JSON
{
  "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"]
}
Success
You now know how to configure TypeScript for any project. The most important settings are target, module, strict,outDir, and rootDir. Start with tsc --init, enable strict, and add options as your project needs them.