The tsconfig.json file is a crucial part of any TypeScript project. It defines the compiler options, project structure, and other configurations that help you build and manage your TypeScript applications effectively. This article will walk you through the various settings available in tsconfig.json, helping you customize your TypeScript experience.

In this article, we will break down the TypeScript configuration and explore various TypeScrpit configuration options.

Basic Structure of a tsconfig.json file

A tsconfig.json file usually looks like this:

{
  "compilerOptions": {
    // Compiler settings
  },
  "include": [
    // Files or patterns to include
  ],
  "exclude": [
    // Files or patterns to exclude
  ],
  "files": [
    // Specific files to include
  ],
  "references": [
    // For project references (monorepos or multi-project setups)
  ]
}
  • compilerOptions: Where you define how TypeScript should behave like module and EcmaScript version.
  • include: Where you explicitly state which files or patterns should be included in the compilation process.
  • exclude: Allows you to exclude specific files or folders from compilation.
  • files: Unlike include, which works with patterns, the files array allows you to specify individual files to compile. It’s rarely used in larger projects as it can become tedious to manage.
  • references: When working with monorepos or multi-project setups, references helps you connect different tsconfig.json files.

In this article, we will focus on the three most import sections: compilerOptions, include and exclude.

compilerOptions

The compilerOptions property contains various settings that control the behavior of the TypeScript compiler. Here are some commonly used options:

allowSyntheticDefaultImports

Allows default imports from modules that may not have a default export, improving compatibility with non-TypeScript modules.

When set to true, allowSyntheticDefaultImports allows you to write an import like:

import fs from 'fs'

instead of:

import * as fs from 'fs'

If the module does not explicitly specify a default export.

baseUrl

Specifies the base directory for non-relative module imports, helping you shorten and simplify import paths.

For example, in the following directory structure:

project
├── app.ts
├── mypack
│   └── funcs.ts
└── tsconfig.json

With "baseUrl": "./", TypeScript will look for files starting at the same folder as the tsconfig.json:

import { myFunc } from "mypack/funcs";
console.log(myFunc());

declaration

When it is set to true, generates .d.ts type definition files for your compiled output, making it easier for other projects to use your code’s types.

For example, running the compiler with this TypeScript code:

export let helloStash = "hello world";

Will generate an index.js file like this:

export let helloStash = "hello world";

With a corresponding helloWorld.d.ts:

export declare let helloStash: string;

downlevelIteration

Enables modern iteration features (like for..of and spread) to be downleveled for older targets, improving compatibility with older JavaScript runtimes.

Without downlevelIteration enabled, the following TypeScript code is compiled into a traditional JavaScript for loop:

const str = "Stash!";
for (const s of str) {
  console.log(s);
}

Output:

var str = "Stash!";
for (var _i = 0, str_1 = str; _i < str_1.length; _i++) {
    var s = str_1[_i];
    console.log(s);
}

When you try to compile the same code with downlevelIteration enabled, the output will look like this:

var __values = (this && this.__values) || function(o) {
    var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
    if (m) return m.call(o);
    if (o && typeof o.length === "number") return {
        next: function () {
            if (o && i >= o.length) o = void 0;
            return { value: o && o[i++], done: !o };
        }
    };
    throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
};
var e_1, _a;
var str = "Stash!";
try {
    for (var str_1 = __values(str), str_1_1 = str_1.next(); !str_1_1.done; str_1_1 = str_1.next()) {
        var s = str_1_1.value;
        console.log(s);
    }
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
    try {
        if (str_1_1 && !str_1_1.done && (_a = str_1.return)) _a.call(str_1);
    }
    finally { if (e_1) throw e_1.error; }
}

emitDecoratorMetadata and experimentalDecorators

emitDecoratorMetadata emits additional metadata for decorators, enabling advanced capabilities like dependency injection and runtime type analysis.

experimentalDecorators allows the use of experimental decorators in your code, enabling advanced patterns and annotations around classes and methods.

For example, the Typescript:

function MyMethod(
  target: any,
  propertyKey: string | symbol,
  descriptor: PropertyDescriptor
) {
  console.log(target);
  console.log(propertyKey);
  console.log(descriptor);
}
 
class StashDemo {
  @MyMethod
  public foo(bar: string) {
    // do nothing
  }
}
 
const app = new StashDemo();

With emitDecoratorMetadata not set to true the emitted JavaScript is:

var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
function MyMethod(target, propertyKey, descriptor) {
    console.log(target);
    console.log(propertyKey);
    console.log(descriptor);
}
var StashDemo = /** @class */ (function () {
    function StashDemo() {
    }
    StashDemo.prototype.foo = function (bar) {
        // do nothing
    };
    __decorate([
        MyMethod
    ], StashDemo.prototype, "foo", null);
    return StashDemo;
}());
var app = new StashDemo();

With both options are set to true:

var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
function MyMethod(target, propertyKey, descriptor) {
    console.log(target);
    console.log(propertyKey);
    console.log(descriptor);
}
var StashDemo = /** @class */ (function () {
    function StashDemo() {
    }
    StashDemo.prototype.foo = function (bar) {
        // do nothing
    };
    __decorate([
        MyMethod,
        __metadata("design:type", Function),
        __metadata("design:paramtypes", [String]),
        __metadata("design:returntype", void 0)
    ], StashDemo.prototype, "foo", null);
    return StashDemo;
}());
var app = new StashDemo();

forceConsistentCasingInFileNames

Ensures file imports match the exact case of filenames, preventing subtle bugs that appear when running on different operating systems.

Let's say you have a file called foo.ts with the following content:

export default function foo() {}

With forceConsistentCasingInFileNames is not set to true, the following TypeScript will compile successfully:

import Foo from './Foo';

Foo()

When you set it to true, the code won't compile unless you use the exact file name:

import Foo from './foo';

Foo()

incremental

When it is set to true, TypeScript creates a series of .tsbuildinfo files in the same folder as your compilation output to speeds up subsequent compilations by storing build information, making development faster after initial compilation.

module

It determines the module system for the emitted JavaScript, influencing how import and export statements are transformed.

For example, the TypeScript:

import foo from './foo';
export const bar = foo();

When you set it to CommonJS, the output will look like:

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.bar = void 0;
var foo_1 = require("./foo");
exports.bar = (0, foo_1.default)();

When you set it to ESNext, the output:

import foo from './foo';
export var bar = foo();

While CommonJS is used for mostly NodeJS projects, ESNext is the standardized module system for JavaScript and are supported natively by modern browsers and Node.js (starting with version 12+ for most use cases).

moduleResolution

It controls how modules are resolved during compilation. Commonly set to "node" for Node.js-like resolution behavior.

noFallthroughCasesInSwitch

When it is set to true, it prevents switch statements from inadvertently falling through to the next case clause, catching a common source of logic errors.

The following TypeScript code won't compile when it is set to true becuase we have a fallthrough case in switch:

let i = 10;

switch (i) {
  case 0:
    console.log('i is zero.');
  case 10:
    console.log('i is ten.');
    break
}

noImplicitAny

It flags variables that don’t have an explicitly defined type, encouraging safer, more intentional typing in your codebase.

For example, the TypeScript:

function foo(bar) {}

The type of bar argument is implicitly any when noImplicitAny is set to false.

If you set it to true, your code won't compile by giving an error like this:

Parameter 'bar' implicitly has an 'any' type.

outDir

It specifies where compiled JavaScript files are placed, helping keep build outputs organized and separate from source code.

removeComments

If it is true, TypeScript compiler strips comments out of the emitted JavaScript, producing cleaner and potentially smaller output files.

For example, the TypeScript:

/**
 * Function comment...
 */
function foo() {

}

// Someother comment
foo()

When you set it to false, the output look like:

/**
 * Function comment...
 */
function foo() {
}
// Someother comment
foo();

When you set it to true, the output:

function foo() {
}
foo();

resolveJsonModule

It allows importing .json files directly, helping you include configuration data or other JSON-based assets in your code.

Because TypeScript does not support resolving JSON files by default, you need to set resolveJsonModule to true.

import * as test from './test.json'

console.log(test)

skipLibCheck

When it is true, TypeScript compiler skips type checking in declaration files (.d.ts), speeding up builds at the expense of potentially overlooking library type inconsistencies.

sourceMap

When it is true, TypeScript compiler generates source map files, enabling you to debug your TypeScript code in browsers or editors as though it’s the original source.

strictBindCallApply

When it is true, TypeScript compiler ensures that functions used with bind, call, or apply adhere to correct argument and return types, reducing subtle runtime issues.

For example, the following TypeScript code will compile when strictBindCallApply is false:

function fun(x: string) {
  return parseInt(x);
}
 
// Return type is 'any'
const number = fun.call(undefined, false);

When you set it to true, the code above won't compile until you provide an argument with the correct type:

// Return type is 'number'
const number = fun.call(undefined, '42');

strictNullChecks

When it is true, TypeScript compiler enforces non-nullable types, ensuring null and undefined aren’t used where not expected, thereby preventing common runtime errors.

When it is false, the following TypeScript code will compile successfully:

let foo: string; // Implicitly allows null or undefined
foo = null; // Valid without strictNullChecks
foo = "John Doe"; // Also valid

But when you set it to true, TypeScript compiler will display an error message like:

error TS2322: Type 'null' is not assignable to type 'string'.

target

target specifies the JavaScript version to compile to, letting you tailor output for older browsers or take advantage of modern language features.

Most modern browsers support all ES6 features, so ES6 is usually a good choice.

Allowed values:

  • es3
  • es5
  • es6/es2015
  • es2016
  • es2017
  • es2018
  • es2019
  • es2020
  • es2021
  • es2022
  • es2023
  • esnext

include and exclude

When you’re working on a project, it’s likely that not every file in your directory is relevant to TypeScript. Maybe you have some raw JavaScript files, Markdown documentation, or assets like images. TypeScript doesn’t need to compile these files—and this is where the include and exclude options come in handy.

These options help the compiler decide which files to consider and which ones to ignore.

Using the include option

The include option explicitly tells TypeScript which files or directories to compile. It accepts an array of glob patterns.

Here’s a simple example:

{
  "include": ["src/**/*"]
}

In this configuration:

  • The src/**/* pattern means "include all files in the src directory and its subdirectories."
  • The **/* part is a glob pattern that matches all files recursively.

By default, if you don’t specify include, TypeScript will include all .ts and .tsx files in the project directory.

You can also specify which file extensions to include.

{
	"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.d.tsx"],
}

Using the exclude option

The exclude option, on the other hand, lets you specify files or directories that TypeScript should ignore. This is particularly useful for ignoring files like test outputs, temporary files, or dependencies in the node_modules folder.

A common example looks like this:

{
  "exclude": ["node_modules", "dist"]
}

In this configuration:

  • node_modules is excluded because you don’t want to compile your dependencies.
  • dist is excluded because it often contains compiled files, which don’t need to be compiled again.

By default, if you don’t specify exclude, TypeScript automatically excludes node_modules and some other system files.

Wrapping up

Your tsconfig.json file is not merely a configuration tool—it’s a critical element in guiding the way TypeScript interprets and compiles your code. When you start working on TypeScript projects, you will often find yourself updating your tsconfig.json file, adding or removing options as needed. Because different projects might require different needs, over time you’ll find a balanced setup that fits your workflow, keeps your code safer, and makes development feel more natural. You can explore all of the available configuration options for your tsconfig.json on TypeScript’s official website: https://www.typescriptlang.org/tsconfig/.

AUTHOR
PUBLISHED 19 February 2025
TOPICS