Skip to main content
Advertisement

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:

  1. Which files to compile: which files to include (include, exclude, files)
  2. 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:

ValueDescriptionKey supported features
ES5ECMAScript 5var, regular functions
ES6 / ES2015ECMAScript 6let, const, arrow functions, classes
ES2017ECMAScript 2017async/await
ES2020ECMAScript 2020Optional chaining (?.), Nullish coalescing (??)
ES2022ECMAScript 2022Top-level await, class fields
ESNextIncludes latest proposalsAll 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:

ValueDescriptionUse case
CommonJSrequire() / module.exportsTraditional Node.js projects
ESNextimport / exportBundler environments (Webpack, Vite)
NodeNextFull Node.js ESM supportModern Node.js projects
NoneNo module systemGlobal 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:

OptionDescription
strictNullChecksTreats null and undefined as separate types
strictFunctionTypesChecks covariance/contravariance of function parameter types
strictBindCallApplyType-checks the bind, call, and apply methods
strictPropertyInitializationChecks that class properties are initialized in the constructor
noImplicitAnyDisallows implicit any when types cannot be inferred
noImplicitThisRequires explicit this type annotations
alwaysStrictEmits "use strict" at the top of every file
useUnknownInCatchVariablesTreats catch clause variables as unknown type
exactOptionalPropertyTypesDisallows 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 valueIncludes
ES2022Standard ES2022 APIs (Array, Promise, Map, etc.)
DOMBrowser DOM APIs (document, window, HTMLElement, etc.)
DOM.IterableIterable types for DOM collections
WebWorkerWeb 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: Enables import 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 checking
  • moduleResolution: "bundler": Module resolution optimized for bundlers like Vite
  • jsx: "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.

ValueDescription
nodeNode.js traditional CommonJS module resolution
node16 / nodenextNode.js 16+ with ESM support
bundlerEnvironments 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

OptionRoleRecommended value
targetOutput JS versionES2022 (for Node.js 20)
moduleModule systemCommonJS (Node.js) / ESNext (bundler)
strictStrict type checkingtrue (always)
outDirCompiled output location"./dist"
rootDirSource file root"./src"
libBuilt-in type definitionsMatch your environment (DOM / ES2022)
sourceMapGenerate source mapstrue (development environments)
esModuleInteropCJS/ESM interoperabilitytrue
skipLibCheckSkip checking .d.ts filestrue
moduleResolutionModule resolution strategybundler (Vite) / node (CJS)

The next chapter covers writing and compiling your first TypeScript file.

Advertisement