1.3 tsconfig.json Basics
The TypeScript compiler can be controlled through dozens of configuration options. The file that holds these settings is tsconfig.json. When this file is present in a project, running tsc with no arguments will compile the project according to whatever settings are in the file. Understanding tsconfig.json properly is the first step in managing a TypeScript project.
What tsconfig.json Does
tsconfig.json defines two things:
- Which files to compile: which files to include (
include,exclude,files) - How to compile them: how the compiler should behave (
compilerOptions)
The directory containing tsconfig.json is treated as the root of the project. When you run tsc, it starts in the current directory and walks up the directory tree looking for a tsconfig.json.
Generating tsconfig.json
Use the tsc --init command to generate a default tsconfig.json.
npx tsc --init
The generated file is hundreds of lines long, but most of the content is commented out. Only a handful of options are actually active. The basic structure with comments removed looks like this:
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
The 5 Essential compilerOptions
1. target — Output JavaScript version
Specifies which version of JavaScript TypeScript should compile down to.
{
"compilerOptions": {
"target": "ES2022"
}
}
Available values:
| Value | Description | Key supported features |
|---|---|---|
ES5 | ECMAScript 5 | var, regular functions |
ES6 / ES2015 | ECMAScript 6 | let, const, arrow functions, classes |
ES2017 | ECMAScript 2017 | async/await |
ES2020 | ECMAScript 2020 | Optional chaining (?.), Nullish coalescing (??) |
ES2022 | ECMAScript 2022 | Top-level await, class fields |
ESNext | Includes latest proposals | All cutting-edge features |
Depending on the target, TypeScript performs downlevel compilation. For example, with target: "ES5", arrow functions are transformed into regular functions.
// TypeScript source
const greet = (name: string) => `Hello, ${name}`;
// Compiled with target: "ES5"
var greet = function(name) { return "Hello, " + name; };
// Compiled with target: "ES2022"
const greet = (name) => `Hello, ${name}`;
If you are using Node.js 20 LTS, ES2022 or ES2023 is recommended. ES2020 or higher is also sufficient when targeting modern browsers.
2. module — Module system
Specifies the module system the compiled JavaScript should use.
{
"compilerOptions": {
"module": "CommonJS"
}
}
Key available values:
| Value | Description | Use case |
|---|---|---|
CommonJS | require() / module.exports | Traditional Node.js projects |
ESNext | import / export | Bundler environments (Webpack, Vite) |
NodeNext | Full Node.js ESM support | Modern Node.js projects |
None | No module system | Global scripts |
module and target should be considered together. If you are using a bundler (Vite, Webpack), module: "ESNext" is the right choice. If running directly in Node.js without a bundler, use module: "CommonJS" or module: "NodeNext".
3. strict — Strict mode
A flag that enables TypeScript's strict type-checking options all at once.
{
"compilerOptions": {
"strict": true
}
}
"strict": true simultaneously enables all 9 of the following options:
| Option | Description |
|---|---|
strictNullChecks | Treats null and undefined as separate types |
strictFunctionTypes | Checks covariance/contravariance of function parameter types |
strictBindCallApply | Type-checks the bind, call, and apply methods |
strictPropertyInitialization | Checks that class properties are initialized in the constructor |
noImplicitAny | Disallows implicit any when types cannot be inferred |
noImplicitThis | Requires explicit this type annotations |
alwaysStrict | Emits "use strict" at the top of every file |
useUnknownInCatchVariables | Treats catch clause variables as unknown type |
exactOptionalPropertyTypes | Disallows explicitly assigning undefined to optional properties |
Without strict, type checking becomes so loose that using TypeScript loses much of its value.
// Dangerous code allowed when strict: false
function processName(name) { // name is implicitly type any
return name.toUpperCase(); // possible runtime error
}
// Error when strict: true
// Error: Parameter 'name' implicitly has an 'any' type.
// Effect of strictNullChecks
function getLength(str: string | null): number {
return str.length;
// strict: true → Error: Object is possibly 'null'
return str?.length ?? 0; // correct approach
}
Always start new projects with "strict": true.
4. outDir — Compiled output directory
Specifies the directory where compiled .js files are placed.
{
"compilerOptions": {
"outDir": "./dist"
}
}
Without outDir, .js files are generated in the same directory as the .ts files, mixing source and output together.
# Without outDir (messy)
src/
├── index.ts
├── index.js ← auto-generated
├── utils.ts
└── utils.js ← auto-generated
# With outDir: "./dist" (clean)
src/
├── index.ts
└── utils.ts
dist/
├── index.js
└── utils.js
5. rootDir — Source file root
Specifies the root directory of TypeScript source files.
{
"compilerOptions": {
"rootDir": "./src"
}
}
rootDir is used together with outDir to preserve the directory structure when compiling.
# With rootDir: "./src" and outDir: "./dist"
src/
├── index.ts
└── utils/
└── helper.ts
→ After compilation
dist/
├── index.js
└── utils/
└── helper.js
Without rootDir, TypeScript automatically uses the common ancestor directory of all input files as the root. This can produce unexpected structures, so it is best to set it explicitly.
include / exclude / files
These options control which files are included in compilation.
include
Specifies the files or patterns to compile. Supports glob patterns.
{
"include": [
"src/**/*",
"types/**/*"
]
}
Without include, all TypeScript files in the directory containing tsconfig.json are compiled.
exclude
Specifies files or patterns to exclude from compilation.
{
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
"**/*.spec.ts"
]
}
Without an explicit exclude, node_modules, bower_components, jspm_packages, and outDir are excluded automatically.
files
Specifies individual files to compile by exact path. Useful when only a small number of files are needed.
{
"files": [
"src/index.ts",
"src/types.ts"
]
}
lib — Built-in type declarations
Specifies the set of built-in type definition libraries TypeScript includes by default.
{
"compilerOptions": {
"lib": ["ES2022", "DOM", "DOM.Iterable"]
}
}
| lib value | Includes |
|---|---|
ES2022 | Standard ES2022 APIs (Array, Promise, Map, etc.) |
DOM | Browser DOM APIs (document, window, HTMLElement, etc.) |
DOM.Iterable | Iterable types for DOM collections |
WebWorker | Web Worker API |
For a Node.js backend project, DOM-related libs are unnecessary. Since there is no browser environment, accessing document or window should be an error.
When lib is not specified, TypeScript automatically sets defaults based on target. With target: "ES2022", the ES2022 lib is included automatically.
sourceMap — Source maps for debugging
Generates source map files. The accompanying .js.map files allow you to debug against the original TypeScript source instead of compiled JavaScript in both browser developer tools and the Node.js debugger.
{
"compilerOptions": {
"sourceMap": true
}
}
With source maps enabled, error stack traces show line numbers from the .ts file rather than the compiled .js file.
Practical Example: tsconfig Configurations for Different Use Cases
Node.js backend project
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"lib": ["ES2022"],
"rootDir": "./src",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"sourceMap": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
module: "CommonJS": Compatible with the traditional Node.js ecosystem- No DOM lib: browser API types are not available
resolveJsonModule: Enablesimport data from './data.json'
Modern Node.js project (ESM)
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"rootDir": "./src",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Add "type": "module" to package.json, and use the .js extension explicitly when importing in .ts files.
// Import with NodeNext module system
import { helper } from './utils/helper.js'; // .js extension required
Vite + React frontend project
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true,
"allowImportingTsExtensions": true
},
"include": ["src"],
"exclude": ["node_modules"]
}
noEmit: true: Vite handles bundling, so tsc only performs type checkingmoduleResolution: "bundler": Module resolution optimized for bundlers like Vitejsx: "react-jsx": React 17+ JSX transform
Pro Tips
Why you should always enable strict mode
Using TypeScript without strict: true is like driving without a seatbelt. Without strictNullChecks in particular, null and undefined can be freely assigned to any type, which means runtime errors are not prevented.
If enabling strict mode all at once is too disruptive when migrating an existing JavaScript project to TypeScript, you can activate individual options one at a time.
{
"compilerOptions": {
"strictNullChecks": true,
"noImplicitAny": true
}
}
Understanding moduleResolution: "bundler"
moduleResolution specifies how TypeScript resolves modules referenced in import statements.
| Value | Description |
|---|---|
node | Node.js traditional CommonJS module resolution |
node16 / nodenext | Node.js 16+ with ESM support |
bundler | Environments using bundlers like Vite, esbuild, or Webpack |
bundler allows imports without file extensions and recognizes the exports field in package.json. Use bundler for frontend projects that use a bundler.
// moduleResolution: "bundler" — extension can be omitted
import { helper } from './utils/helper';
// moduleResolution: "nodenext" — extension is required
import { helper } from './utils/helper.js';
Reuse configuration with tsconfig inheritance
When splitting tsconfig across multiple environments (development, production, testing), use extends to inherit from a base configuration.
// tsconfig.base.json
{
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
// tsconfig.json (development)
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"sourceMap": true,
"outDir": "./dist"
},
"include": ["src/**/*"]
}
// tsconfig.test.json (testing)
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"types": ["jest", "node"]
},
"include": ["src/**/*", "tests/**/*"]
}
Summary
| Option | Role | Recommended value |
|---|---|---|
target | Output JS version | ES2022 (for Node.js 20) |
module | Module system | CommonJS (Node.js) / ESNext (bundler) |
strict | Strict type checking | true (always) |
outDir | Compiled output location | "./dist" |
rootDir | Source file root | "./src" |
lib | Built-in type definitions | Match your environment (DOM / ES2022) |
sourceMap | Generate source maps | true (development environments) |
esModuleInterop | CJS/ESM interoperability | true |
skipLibCheck | Skip checking .d.ts files | true |
moduleResolution | Module resolution strategy | bundler (Vite) / node (CJS) |
The next chapter covers writing and compiling your first TypeScript file.