tsconfig.json Essentials

Difficulty: Advanced

The tsconfig.json file is the central configuration file for any TypeScript project. It tells the TypeScript compiler how to process your code - what JavaScript version to target, which module system to use, where to find source files, and where to put compiled output. Understanding the essential compiler options is critical for setting up projects correctly and debugging configuration issues.

The `target` option specifies the ECMAScript version for the output JavaScript. Common values are 'ES2020', 'ES2022', or 'ESNext'. When you set a target, TypeScript downlevels newer syntax (like optional chaining or nullish coalescing) to work in the target environment. The `lib` option specifies which built-in API declarations to include - for example, 'DOM' includes browser APIs, 'ES2020' includes Promise.allSettled, and 'ES2021' includes String.replaceAll. If you target ES2020 but need DOM APIs, set lib to ['ES2020', 'DOM', 'DOM.Iterable'].

The `module` option determines the module system for the output code: 'CommonJS' for Node.js require/exports, 'ESNext' or 'ES2020' for native ES modules, or 'NodeNext' for Node.js with ESM support. This works in conjunction with `moduleResolution` which determines how imports are resolved. The `outDir` option specifies where compiled .js files are written, and `rootDir` specifies the root of input files. TypeScript preserves the directory structure relative to rootDir when writing to outDir.

Other essential options include `include` and `exclude` arrays that determine which files TypeScript processes, `declaration` for generating .d.ts files, `sourceMap` for debugging support, and `esModuleInterop` which enables interoperability between CommonJS and ES module import syntax. The `skipLibCheck` option skips type checking of .d.ts files, significantly speeding up compilation in large projects with many dependencies.

Code examples

Essential tsconfig.json Options

// A typical tsconfig.json for a modern project:
// {
//   "compilerOptions": {
//     "target": "ES2020",
//     "module": "ESNext",
//     "lib": ["ES2020", "DOM", "DOM.Iterable"],
//     "outDir": "./dist",
//     "rootDir": "./src",
//     "strict": true,
//     "esModuleInterop": true,
//     "skipLibCheck": true,
//     "forceConsistentCasingInFileNames": true,
//     "declaration": true,
//     "sourceMap": true
//   },
//   "include": ["src//*"],
//   "exclude": ["node_modules", "dist"]
// }

// Demonstrating how target affects output
interface Config {
  target: string;
  module: string;
  lib: string[];
  outDir: string;
  rootDir: string;
}

const config: Config = {
  target: 'ES2020',
  module: 'ESNext',
  lib: ['ES2020', 'DOM', 'DOM.Iterable'],
  outDir: './dist',
  rootDir: './src'
};

console.log(`Target: ${config.target}`);
console.log(`Module: ${config.module}`);
console.log(`Lib: ${config.lib.join(', ')}`);
console.log(`Output: ${config.outDir}`);
console.log(`Source: ${config.rootDir}`);

These five options form the core of any tsconfig: target sets the JS output version, module sets the module system, lib declares available APIs, outDir is where output goes, rootDir is where source lives.

Target Downleveling

// TypeScript downlevels syntax based on the target
// With target: ES2015, optional chaining is compiled away
// With target: ES2020, optional chaining is kept as-is

interface User {
  name: string;
  address?: {
    city?: string;
    zip?: string;
  };
}

// Optional chaining (ES2020+)
const user1: User = { name: 'Alice', address: { city: 'NYC' } };
const user2: User = { name: 'Bob' };

// These use optional chaining - kept as-is for ES2020 target
console.log(user1.address?.city ?? 'Unknown');
console.log(user2.address?.city ?? 'Unknown');

// Nullish coalescing (ES2020+)
const port: number | undefined = undefined;
const resolvedPort = port ?? 3000;
console.log(`Port: ${resolvedPort}`);

// If target were ES2015, the above would compile to:
// user1.address !== null && user1.address !== void 0 ? user1.address.city : undefined
console.log('All modern syntax works with ES2020 target');

The target option determines which syntax features are kept and which are downleveled. ES2020 supports optional chaining (?.) and nullish coalescing (??).

Module and Lib Interaction

// The lib option determines which global types are available
// Without 'DOM', you cannot use document, window, fetch, etc.
// Without 'ES2020', you cannot use Promise.allSettled, BigInt, etc.

// Simulating lib availability
const availableAPIs: Record<string, string[]> = {
  'ES2015': ['Promise', 'Map', 'Set', 'Symbol', 'WeakMap'],
  'ES2020': ['Promise.allSettled', 'BigInt', 'globalThis', 'matchAll'],
  'ES2021': ['String.replaceAll', 'Promise.any', 'WeakRef'],
  'ES2022': ['Array.at', 'Object.hasOwn', 'Error.cause'],
  'DOM': ['document', 'window', 'fetch', 'localStorage']
};

const selectedLibs = ['ES2020', 'DOM'];
const allAPIs = selectedLibs.flatMap(lib => availableAPIs[lib] || []);

console.log(`Libs: ${selectedLibs.join(', ')}`);
console.log(`Available APIs: ${allAPIs.join(', ')}`);
console.log(`Total APIs: ${allAPIs.length}`);

The lib array is additive - each entry includes a set of type declarations. You must include both the ES version and DOM if building for the browser.

Key points

Concepts covered

target, module, lib, outDir, rootDir, tsconfig.json, compiler options