TS Config #

tsconfig.json is the control center of the entire TypeScript compiler behavior — it determines which files get compiled, which JavaScript version the output targets, how strict type checking is, and much more. Understanding tsconfig.json deeply isn’t just about configuring a project from scratch, but also about understanding why a project you inherited behaves the way it does. Many seemingly confusing TypeScript problems — strange errors, broken import paths, or unexpected JavaScript output — all stem from tsconfig.json configuration. This article covers all the important options along with their practical implications and recommendations.

The Basic Structure of tsconfig.json #

Generate an initial configuration file with:

npx tsc --init

This produces a tsconfig.json with all options commented out. Here’s the complete structure and the relationships between its main parts:

flowchart TD
    A[tsconfig.json] --> B[compilerOptions]
    A --> C[include]
    A --> D[exclude]
    A --> E[files]
    A --> F[extends]
    A --> G[references]

    B --> B1[Target & Output]
    B --> B2[Type Checking]
    B --> B3[Module Resolution]
    B --> B4[Source Maps & Debug]
    B --> B5[Paths & Aliases]

    C --> C1[Glob: src/**/*]
    D --> D1[node_modules\ndist\n*.spec.ts]
    F --> F1[Base config\nfrom another file]
    G --> G1[Project references\nfor monorepos]

    style B fill:#339af0,color:#fff
    style B2 fill:#ff6b6b,color:#fff
    style F fill:#51cf66,color:#fff

compilerOptions — Target and Output Options #

This group of options controls which JavaScript version the TypeScript code is compiled to and where the results are stored:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "CommonJS",
    "lib": ["ES2022"],
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "declarationDir": "./dist/types",
    "sourceMap": true,
    "removeComments": false,
    "noEmitOnError": true
  }
}

target — The JavaScript Output Version #

target determines which JavaScript features are allowed in the output. Features newer than the target get downleveled (transpiled to an older equivalent):

Target       Use for                         Features transpiled
────────────────────────────────────────────────────────────────────
ES5          Old browsers, wide compatibility   Arrow functions, class, const, let, etc.
ES2015/ES6   Node.js 6+, modern browsers         async/await, generators
ES2016       Node.js 8+                         Async iteration
ES2017       Node.js 8+                         Object.values, Object.entries
ES2019       Node.js 12+                        Optional catch binding
ES2020       Node.js 14+                        Optional chaining, nullish coalescing
ES2022       Node.js 16+                        Top-level await, class fields
ESNext       Always the latest version          Minimal transpilation

module — The Module System #

module determines how import/export statements are compiled in the output:

CommonJS    Traditional Node.js — require/module.exports
ES2020      Modern ES modules — import/export (needs Node.js 12+ with "type":"module")
NodeNext    Node.js ESM aware of .cts/.mts — recommended for modern Node.js
Bundler     For Vite, Webpack, esbuild — doesn't resolve modules itself

lib — Available APIs #

lib determines which built-in type definitions are available. If not set, TypeScript determines them automatically based on target:

{
  "compilerOptions": {
    // For Node.js backends (no DOM needed)
    "lib": ["ES2022"],

    // For browsers / frontend
    "lib": ["ES2022", "DOM", "DOM.Iterable"],

    // To use fetch in Node.js 18+
    "lib": ["ES2022", "DOM"]
  }
}

declaration and declarationDir #

Important for libraries published to npm — generates .d.ts files that give library users type safety:

{
  "compilerOptions": {
    "declaration": true,           // Generate .d.ts files
    "declarationDir": "./dist/types", // Location of .d.ts files
    "declarationMap": true         // Source map for .d.ts (makes "Go to Definition" easier)
  }
}

sourceMap and inlineSourceMap #

{
  "compilerOptions": {
    "sourceMap": true,        // Generate separate .js.map files
    // OR
    "inlineSourceMap": true,  // Embed the source map directly in .js files (not recommended for production)
    "inlineSources": true     // Include TypeScript sources inside the source map
  }
}

compilerOptions — Type Checking #

This is the group of options that most affects the safety of your TypeScript code:

{
  "compilerOptions": {
    // === MUST ENABLE — activates all the following strict checks at once ===
    "strict": true,

    // Individual strict options (already included in strict: true)
    "noImplicitAny": true,           // Forbid implicitly inferred 'any' types
    "strictNullChecks": true,        // null/undefined can't enter other types
    "strictFunctionTypes": true,     // Check function parameter types contravariantly
    "strictBindCallApply": true,     // Check types for bind/call/apply
    "strictPropertyInitialization": true, // Class properties must be initialized in the constructor
    "noImplicitThis": true,          // 'this' with an implicit 'any' type is an error
    "useUnknownInCatchVariables": true,   // Errors in catch are typed 'unknown' not 'any'
    "alwaysStrict": true,            // Parse in strict mode and emit "use strict"

    // Additional options beyond strict
    "noUnusedLocals": true,          // Error for unused local variables
    "noUnusedParameters": true,      // Error for unused parameters
    "noImplicitReturns": true,       // All return paths must return a value
    "noFallthroughCasesInSwitch": true, // Forbid fall-through in switch without break
    "noUncheckedIndexedAccess": true, // arr[i] is typed T | undefined (safer)
    "noPropertyAccessFromIndexSignature": true, // Forbid dot notation for index signatures
    "exactOptionalPropertyTypes": true, // Distinguish undefined from an absent property
    "noImplicitOverride": true       // Require writing 'override' for overridden methods
  }
}

Practical Implications of strict: true #

// With strict: true, all of these become errors caught by the compiler:

// noImplicitAny — no implicit any
function proses(x) { return x; }
// ✗ Error: Parameter 'x' implicitly has an 'any' type

// strictNullChecks — no unhandled null/undefined
const nama: string = null;
// ✗ Error: Type 'null' is not assignable to type 'string'

// useUnknownInCatchVariables — catch errors are typed unknown
try { } catch (e) {
  e.message; // ✗ Error: 'e' is of type 'unknown'
}

// noUncheckedIndexedAccess — array/object index access is safer
const arr: number[] = [1, 2, 3];
const val = arr[10]; // Type: number | undefined — not just number
Enable strict: true from day one of a project. Enabling it on an already-running project requires fixing hundreds or thousands of errors at once. If you must enable it gradually on an old project, enable one strict option per sprint — starting with noImplicitAny, then strictNullChecks, and so on.

compilerOptions — Module Resolution #

{
  "compilerOptions": {
    "moduleResolution": "bundler",  // Or "node", "node16", "nodenext"
    "esModuleInterop": true,        // Allow default imports from CommonJS modules
    "allowSyntheticDefaultImports": true, // Allow default imports from modules without defaults
    "resolveJsonModule": true,      // Allow importing .json files
    "allowJs": true,                // Allow .js files in a TypeScript project
    "checkJs": false,               // Don't type-check .js files (useful during migration)
    "forceConsistentCasingInFileNames": true // Error for imports with different capitalization
  }
}

moduleResolution — How TypeScript Finds Modules #

node        — The classic Node.js algorithm (require), suited to old CommonJS
node16      — Node.js 16+ ESM-aware, needs explicit extensions in imports
nodenext    — The latest for Node.js ESM, an alias of node16
bundler     — For Vite/webpack/esbuild, no extensions needed, flexible resolution

Path Aliases with baseUrl and paths #

Path aliases enable clean imports without long ../../../ chains:

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

With the configuration above:

// Without aliases — verbose and fragile when refactoring
import { format } from "../../../utils/format";

// With aliases — clean and independent of file location
import { format } from "@utils/format";
import { Button } from "@components/Button";
import type { Pengguna } from "@types/pengguna";
Path aliases in tsconfig.json are only recognized by the TypeScript compiler — not by the Node.js runtime or bundlers automatically. For Node.js, you need tsconfig-paths or @swc/register. For bundlers like Vite, Webpack, or esbuild, you need to configure the same aliases in that bundler’s configuration.

compilerOptions — Code Quality #

{
  "compilerOptions": {
    "skipLibCheck": true,             // Skip type checking .d.ts files in node_modules
    "noEmitOnError": true,            // Don't produce output if there are errors
    "removeComments": false,          // Keep comments in the output (useful for debugging)
    "stripInternal": true,            // Remove declarations marked @internal from .d.ts
    "experimentalDecorators": true,   // Enable decorators (needed by NestJS, etc.)
    "emitDecoratorMetadata": true,    // Emit metadata for decorators (needed by NestJS)
    "useDefineForClassFields": true   // Use defineProperty for class fields (ES2022+)
  }
}

include, exclude, and files #

{
  // include — files/directories that get compiled (default: all .ts in the tsconfig directory)
  "include": [
    "src/**/*",           // All files in src/ and its subdirectories
    "scripts/**/*.ts"     // Additional scripts
  ],

  // exclude — files/directories EXCLUDED from compilation
  // The default already includes: node_modules, outDir, and files from the exclude field
  "exclude": [
    "node_modules",
    "dist",
    "**/*.spec.ts",    // Test files don't need compiling for production
    "**/*.test.ts"
  ],

  // files — an explicit file list (used with include or on its own)
  // Rarely used — more often you use include with globs
  "files": [
    "src/index.ts",
    "src/types/global.d.ts"
  ]
}

extends — Inheriting Configuration #

extends lets one tsconfig.json inherit all settings from another file. This is very useful for:

  1. Sharing a base config across multiple projects in a monorepo
  2. Overriding some options for different environments (dev vs production)
  3. Using community presets like @tsconfig/node20
// tsconfig.base.json — shared configuration
{
  "compilerOptions": {
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true,
    "resolveJsonModule": true
  }
}
// tsconfig.json — extends the base, adds specific options
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "target": "ES2022",
    "module": "CommonJS",
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
// tsconfig.test.json — for testing with Jest/Vitest
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "noEmit": true,    // Don't produce output when type-checking tests
    "types": ["jest"]  // Add Jest type definitions
  },
  "include": ["src/**/*", "tests/**/*", "**/*.spec.ts", "**/*.test.ts"]
}

Community Presets #

# Install the preset for Node.js 20
npm install --save-dev @tsconfig/node20

# Use it in tsconfig.json
{
  "extends": "@tsconfig/node20/tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src"
  }
}

Project References — For Monorepos #

Project references let TypeScript understand dependencies between packages in a monorepo, enabling very fast incremental compilation:

monorepo/
├── packages/
│   ├── shared/          # Shared library
│   │   ├── src/
│   │   └── tsconfig.json
│   ├── backend/         # Backend service
│   │   ├── src/
│   │   └── tsconfig.json
│   └── frontend/        # Frontend app
│       ├── src/
│       └── tsconfig.json
└── tsconfig.json        # Root tsconfig
// Root tsconfig.json — orchestration only
{
  "files": [],           // No files compiled at the root
  "references": [
    { "path": "./packages/shared" },
    { "path": "./packages/backend" },
    { "path": "./packages/frontend" }
  ]
}
// packages/backend/tsconfig.json
{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "composite": true,     // Required for project references
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "references": [
    { "path": "../shared" }  // The backend depends on shared
  ]
}

Ready-to-Use Configuration Templates #

For Node.js Backends (Express, Fastify, NestJS) #

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "CommonJS",
    "lib": ["ES2022"],
    "moduleResolution": "node",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "sourceMap": true,
    "declaration": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "noEmitOnError": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"]
}

For Frontend (React, Vue with Vite) #

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "allowImportingTsExtensions": true,
    "noEmit": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

For npm Libraries (Published) #

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ES2020",
    "lib": ["ES2020"],
    "moduleResolution": "bundler",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "noEmitOnError": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
}

Important Options Reference Table #

OptionDefaultRecommendationDescription
strictfalsetrueEnable all strict checks
targetES3ES2022JS output version
moduleDepends on targetCommonJS/NodeNextModule system
outDir./distOutput directory
rootDir./srcSource directory
sourceMapfalsetrueSource map for debugging
noEmitOnErrorfalsetrueDon’t emit if there are errors
esModuleInteropfalsetrueCommonJS interop
skipLibCheckfalsetrueSkip checking node_modules
noUncheckedIndexedAccessfalsetrueSafer array indexing
noUnusedLocalsfalsetrueCatch unused variables
forceConsistentCasingInFileNamesfalsetrueFile name consistency

Summary #

  • strict: true is the most important option — it activates eight strict checks at once; enable it from day one of a project, don’t wait.
  • target determines which JS features get transpiled — use ES2022 for Node.js 16+ or modern browsers; TypeScript downlevels newer features automatically.
  • noEmitOnError: true must be enabled to prevent JavaScript output from being produced when there are type errors — without it you could accidentally deploy broken code.
  • noUncheckedIndexedAccess isn’t included in strict but is highly recommended — it makes array/object index access yield the more accurate T | undefined.
  • Path aliases in tsconfig aren’t recognized at runtime — you need extra configuration in the bundler or use tsconfig-paths for Node.js.
  • Use extends to share a base configuration across environments and projects — keep one tsconfig.base.json and override as needed in tsconfig.json, tsconfig.test.json, and so on.
  • skipLibCheck: true speeds up compilation by skipping type checks on .d.ts files in node_modules — safe to enable because type issues in libraries are usually already fixed in recent versions.
  • sourceMap: true is mandatory for effective debugging — without it stack traces point to the compiled JavaScript, not the original TypeScript.
  • For monorepos, use project references with composite: true — this enables much faster incremental compilation because TypeScript only recompiles changed packages.

← Previous: Regex Identifier   Next: Vendoring →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact