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:#fffcompilerOptions — 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
Enablestrict: truefrom 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 withnoImplicitAny, thenstrictNullChecks, 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 intsconfig.jsonare only recognized by the TypeScript compiler — not by the Node.js runtime or bundlers automatically. For Node.js, you needtsconfig-pathsor@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:
- Sharing a base config across multiple projects in a monorepo
- Overriding some options for different environments (dev vs production)
- 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 #
| Option | Default | Recommendation | Description |
|---|---|---|---|
strict | false | true | Enable all strict checks |
target | ES3 | ES2022 | JS output version |
module | Depends on target | CommonJS/NodeNext | Module system |
outDir | — | ./dist | Output directory |
rootDir | — | ./src | Source directory |
sourceMap | false | true | Source map for debugging |
noEmitOnError | false | true | Don’t emit if there are errors |
esModuleInterop | false | true | CommonJS interop |
skipLibCheck | false | true | Skip checking node_modules |
noUncheckedIndexedAccess | false | true | Safer array indexing |
noUnusedLocals | false | true | Catch unused variables |
forceConsistentCasingInFileNames | false | true | File name consistency |
Summary #
strict: trueis the most important option — it activates eight strict checks at once; enable it from day one of a project, don’t wait.targetdetermines which JS features get transpiled — useES2022for Node.js 16+ or modern browsers; TypeScript downlevels newer features automatically.noEmitOnError: truemust be enabled to prevent JavaScript output from being produced when there are type errors — without it you could accidentally deploy broken code.noUncheckedIndexedAccessisn’t included instrictbut is highly recommended — it makes array/object index access yield the more accurateT | undefined.- Path aliases in
tsconfigaren’t recognized at runtime — you need extra configuration in the bundler or usetsconfig-pathsfor Node.js.- Use
extendsto share a base configuration across environments and projects — keep onetsconfig.base.jsonand override as needed intsconfig.json,tsconfig.test.json, and so on.skipLibCheck: truespeeds up compilation by skipping type checks on.d.tsfiles innode_modules— safe to enable because type issues in libraries are usually already fixed in recent versions.sourceMap: trueis 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.