TypeScript — tsconfig Configuration
tsconfig.json configures the TypeScript compiler (tsc). It defines which files to compile, how to compile them, and what strictness level to enforce. Understanding these options is essential for any TypeScript project.
A minimal tsconfig.json specifies compiler options and which files to include. The compilerOptions object is the heart of the configuration.
| 1 | { |
| 2 | "compilerOptions": { |
| 3 | "target": "ES2020", |
| 4 | "module": "ESNext", |
| 5 | "moduleResolution": "bundler", |
| 6 | "strict": true, |
| 7 | "esModuleInterop": true, |
| 8 | "skipLibCheck": true, |
| 9 | "forceConsistentCasingInFileNames": true, |
| 10 | "outDir": "./dist", |
| 11 | "rootDir": "./src" |
| 12 | }, |
| 13 | "include": ["src/**/*"], |
| 14 | "exclude": ["node_modules", "dist"] |
| 15 | } |
info
target determines the ECMAScript version your code compiles down to. module determines how module syntax (import/export) is transformed.
| 1 | { |
| 2 | "compilerOptions": { |
| 3 | "target": "ES2022", |
| 4 | "module": "ESNext", |
| 5 | "lib": ["ES2022", "DOM", "DOM.Iterable"], |
| 6 | "moduleResolution": "bundler" |
| 7 | } |
| 8 | } |
| target | Description |
|---|---|
| ES5 | Legacy — transforms async/await, classes, etc. |
| ES2017 | Native async/await support |
| ES2020 | Adds BigInt, optional chaining, nullish coalescing |
| ES2022 | Top-level await, class fields, error cause |
| ESNext | Latest features — use only when target runtime supports it |
| module | Description |
|---|---|
| CommonJS | Node.js require/module.exports |
| ESNext | Preserves import/export — let the bundler handle it |
| Node16 / NodeNext | Node.js ESM with .mjs/.cjs resolution |
| UMD | Universal module definition (browser + Node) |
best practice
The strict flag enables a group of strict type-checking options. You can also enable each individually for finer control.
| 1 | { |
| 2 | "compilerOptions": { |
| 3 | "strict": true, |
| 4 | // Equivalent to enabling all of: |
| 5 | "noImplicitAny": true, |
| 6 | "strictNullChecks": true, |
| 7 | "strictFunctionTypes": true, |
| 8 | "strictBindCallApply": true, |
| 9 | "strictPropertyInitialization": true, |
| 10 | "noImplicitThis": true, |
| 11 | "useUnknownInCatchVariables": true |
| 12 | } |
| 13 | } |
| Option | What it does |
|---|---|
| strictNullChecks | null/undefined are distinct types — must handle explicitly |
| noImplicitAny | Error when TypeScript can't infer a type (no hidden any) |
| strictFunctionTypes | Function parameter types are checked contravariantly |
| strictPropertyInitialization | Class properties must be initialized in constructor |
| noImplicitThis | Error when this has an implicit any type |
| useUnknownInCatchVariables | Catch clause variables are unknown instead of any |
warning
Module resolution determines how TypeScript finds imported modules. paths and baseUrl let you create import aliases instead of relative paths.
| 1 | { |
| 2 | "compilerOptions": { |
| 3 | "moduleResolution": "bundler", |
| 4 | "baseUrl": ".", |
| 5 | "paths": { |
| 6 | "@/*": ["./src/*"], |
| 7 | "@components/*": ["./src/components/*"], |
| 8 | "@utils/*": ["./src/utils/*"], |
| 9 | "@lib/*": ["./src/lib/*"] |
| 10 | } |
| 11 | } |
| 12 | } |
| 1 | // Without paths — verbose relative imports |
| 2 | import { Button } from "../../../components/Button"; |
| 3 | import { formatDate } from "../../../utils/date"; |
| 4 | import { db } from "../../../lib/database"; |
| 5 | |
| 6 | // With paths — clean absolute-style imports |
| 7 | import { Button } from "@components/Button"; |
| 8 | import { formatDate } from "@utils/date"; |
| 9 | import { db } from "@lib/database"; |
| 10 | |
| 11 | // Wildcard patterns match any depth |
| 12 | import { helper } from "@utils/helpers/deep/module"; |
best practice
These options control what files TypeScript emits and where.
| 1 | { |
| 2 | "compilerOptions": { |
| 3 | "outDir": "./dist", |
| 4 | "rootDir": "./src", |
| 5 | "declaration": true, |
| 6 | "declarationMap": true, |
| 7 | "sourceMap": true, |
| 8 | "removeComments": false, |
| 9 | "noEmit": false, |
| 10 | "isolatedModules": true |
| 11 | } |
| 12 | } |
| Option | Description |
|---|---|
| outDir | Output directory for compiled files |
| rootDir | Root directory for source files — controls output directory structure |
| declaration | Emit .d.ts declaration files |
| declarationMap | Emit .d.ts.map files for declaration source maps |
| sourceMap | Emit .js.map source map files |
| isolatedModules | Ensure each file can be transpiled independently (required by esbuild/swc) |
| noEmit | Type-check only — don't emit output files |
Source maps let you debug your original TypeScript code in browser DevTools or Node.js debuggers. They map compiled JavaScript back to your source files.
| 1 | { |
| 2 | "compilerOptions": { |
| 3 | "sourceMap": true, |
| 4 | "declarationMap": true, |
| 5 | "inlineSourceMap": false, |
| 6 | "inlineSources": true, |
| 7 | "sourceRoot": "./src" |
| 8 | } |
| 9 | } |
| Option | Description |
|---|---|
| sourceMap | Emit separate .js.map files |
| inlineSourceMap | Embed source map as base64 in the .js file (single file, larger) |
| declarationMap | Source maps for .d.ts files — enables go-to-definition into source |
| inlineSources | Include original .ts source in the source map |
info
TypeScript provides compiler options that enforce code quality beyond type checking. These act as a lightweight linter directly in tsc.
| 1 | { |
| 2 | "compilerOptions": { |
| 3 | "noUnusedLocals": true, |
| 4 | "noUnusedParameters": true, |
| 5 | "noImplicitReturns": true, |
| 6 | "noFallthroughCasesInSwitch": true, |
| 7 | "noUncheckedIndexedAccess": true, |
| 8 | "exactOptionalPropertyTypes": false, |
| 9 | "noPropertyAccessFromIndexSignature": false |
| 10 | } |
| 11 | } |
| Option | Description |
|---|---|
| noUnusedLocals | Error on unused local variables |
| noUnusedParameters | Error on unused function parameters (prefix with _ to allow) |
| noImplicitReturns | Error if a function doesn't explicitly return on all code paths |
| noFallthroughCasesInSwitch | Error on fallthrough cases in switch statements |
| noUncheckedIndexedAccess | Array/object index access returns T | undefined |
For React projects, configure JSX transformation to match your framework's expectations.
| 1 | { |
| 2 | "compilerOptions": { |
| 3 | "jsx": "react-jsx", |
| 4 | "jsxImportSource": "react" |
| 5 | } |
| 6 | } |
| jsx | Description |
|---|---|
| react-jsx | JSX transform for React 17+ (no import React needed) |
| react-jsxdev | Development mode JSX transform with extra debugging info |
| preserve | Keep JSX in output — let a bundler handle transformation |
| react-native | React Native — preserves JSX with .jsx extension |
These guidelines help you set up tsconfig for maintainable, type-safe projects.
Always use strict mode. Start with "strict": true in every project. It catches real bugs. If legacy code needs exceptions, use per-file // @ts-ignore comments temporarily.
Use isolatedModules for bundler compatibility. Modern bundlers (esbuild, swc, Vite) transpile files independently. isolatedModules: true ensures your code is compatible.
Extend from a shared base. Use extends to share config across packages in a monorepo. Keep strict: true in the base and only customize per-package options.
Separate tsconfig files by concern. Use tsconfig.base.json for shared options, tsconfig.app.json for app code, and tsconfig.test.json for tests with different lib/settings.
Don't skip libCheck. skipLibCheck: true skips checking declaration files. It speeds up compilation but hides type errors in third-party types. Enable it for CI speed, but review errors periodically.