• Home
  • About

[Everything About tsconfig] Root Fields


[Everything About tsconfig] Root Fields

It’s been about four years since I started using TypeScript. I fell in love with the powerful static type checking that TypeScript provides — something JavaScript doesn’t have — and I’ve written quite a few projects in TypeScript since then.

A few days ago, while creating a new TypeScript project and naturally setting up tsconfig as usual, something wasn’t working as expected. While spending time looking through the official tsconfig docs, this thought suddenly hit me.

jordan peele Wait... do I actually know what I'm using...?

Maybe it’s just me, but when you work on various projects, you don’t often hand-write tsconfig from scratch anymore.

Of course, when you first developed a project in TypeScript, you probably wrote it carefully by hand. (Actually, it’s been so long I barely remember…) But at some point, I started just copy-pasting tsconfig from existing working projects into new projects and doing only slight customization myself.

So I decided to return to basics and carefully examine what each option in tsconfig actually does to my TypeScript projects.

I wanted to cover everything about tsconfig in a single post, but this thing has so many compiler options that the post ended up exceeding 500 lines. So in this post, I’ll only examine the root-level fields in tsconfig, and continue explaining compiler options in subsequent posts.

include

Type Default
string[] [], ['**/*']
{
  "include": [
    "src/**/*",
    "tests/**/*",
  ]
}

The include field declares which files to include in your TypeScript application. Its default value changes based on whether the files field exists: if files is declared, the default is [], otherwise it’s ["**/*"].

This field does something similar to files, but include lets you use glob syntax to express patterns for file paths, so people generally use include over files.

Glob syntax uses wildcards like * or ? to represent patterns for multiple file names. The syntax itself is similar to regular expressions, so it’s not too hard to learn.

Wildcard Description Example Matching filenames
* Matches 0+ characters excluding / types*.ts types.d.ts, types.ts, types.test.ts
** Matches 0+ characters including / src/**/index.ts src/index.ts, src/utils/index.ts, src/test/index.ts
? Matches a single character, same as . in regex ?at.ts Cat.ts, Bat.ts, Rat.ts
[ab] Matches one character in [] `[C B]at.ts`
[a-z] Matches character range in [] test[0-9].ts test0.ts, test1.ts, test9.ts
{ab,bc} Matches one string in {} *.{ts,tsx} foo.ts, foo.tsx

Glob syntax is useful in almost every situation where you need to match files through file paths. Once you get familiar with it, configuration files you encounter while programming will feel much friendlier.

files

Type Default
string[] false
{
  "files": [
    "./src/index.ts",
    "./src/utils.ts",
    "./src/models.ts"
  ]
}

The files field explicitly declares which files to include in your TypeScript application. It’s similar to includes, but files can’t use glob syntax like src/**/*, so you have to enter each filename individually — a disadvantage.

If the files field has values, the includes field’s default changes from ['**/*'] to an empty array []. If you were relying on the default value of the includes field, adding values to the files field could completely change your compilation output. Keep this in mind.

exclude

Type Default
string[] ['node_modules', 'bower_components', 'jspm_packages']
{
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "src/**/*.test.ts",
    "node_modules"
  ]
}

The exclude field, true to its dictionary meaning of “exclude,” lets you configure “don’t include these files” among files declared in the include field.

Looking at this field’s default values — node_modules, bower_components, jspm_packages — you can see that TypeScript packages are typically built and distributed as a combination of js + *.d.ts files. TypeScript assumes these external packages are already built.

If you need to compile external packages, modify the exclude field. (By the way, Next.js sets up projects with node_modules in this field by default, and even if you remove it, it automatically adds it back during build…)

One important thing: the exclude field just means “don’t look for this” when finding files set in the include field, so files in this field aren’t necessarily excluded from your application.

Even with directories like node_modules set in the exclude field, if you directly import such modules in code using import statements, or use triple-slash directives like /// <reference path="..." /> to directly instruct the compiler to include specific modules, or explicitly specify files to compile using the files field, TypeScript ignores the exclude field settings and imports those modules.

extends

Type Default
string false
// tsconfig.base.json
{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true
  }
}
// tsconfig.json
{
  // Extends settings from tsconfig.base.json
  "extends": "./tsconfig.base.json",
  "files": [
    "./src/main.ts"
  ]
}

The extends field, as its name suggests, lets you specify a path to another tsconfig and inherit from it. Personally, I’ve mainly used this when taking settings someone else made and customizing them slightly, or when there are settings I want to apply to all projects in a monorepo.

If the inherited tsconfig.base.json has relative paths like ./src in its internal fields, those relative paths are calculated based on the tsconfig.json that’s doing the inheriting.

Also, when defining a new tsconfig by inheriting from another tsconfig, fields declared using arrays like files, include, exclude aren’t extended — the fields themselves are overwritten. Keep this in mind.

// tsconfig.base.json
{
  "files": [
    "./src/main.ts"
  ]
}
// tsconfig.json
{
  "extends": "./tsconfig.base.json",
  "files": [
    "./src/index.ts"
  ]
}

In this situation, the final files field isn’t merged into ["./src/main.ts", "./src/index.ts"] but evaluated as ["./src/index.ts"]. Looking at the field name extends, you’d think it would extend the field values, but it just coolly overwrites them without any warning or error. Be careful.

references

Type Default
Array<{ path: string; prepend: boolean; }> false
// my-project/test/tsconfig.json
{
  "references": [
    // References my-project/src/tsconfig.json
    { "path": "../src" }
  ]
}

The reference field expresses reference relationships between modules when using multiple tsconfig files for different modules within the same project. Since most projects use only one tsconfig, this is rarely used.

TypeScript provides this option for situations like this:

├── src/
│   ├── converter.ts
│   └── units.ts
├── test/
│   ├── converter-tests.ts
│   └── units-tests.ts
└── tsconfig.json

This project has source code and test code separated into different directories, with one root tsconfig managing compilation options for the entire project. This is a fairly common structure, but once you build it and start developing, a few inconveniences emerge:

  1. Source code in the src module can import test code from the test module…
  2. Even when modifying parts of source code that never error, test code has to be type-checked again…
  3. Conversely, fixing test code means source code has to be type-checked again…

These inconveniences are manageable normally, but as the project grows, the compiler’s type checking proportionally slows down, potentially making development itself difficult. So what if we create separate tsconfig files for each project module and compile separately?

├── src/
│   ├── converter.ts
│   ├── units.ts
│   └── tsconfig.json
└── test/
    ├── converter-tests.ts
    ├── units-tests.ts
    └── tsconfig.json

Configuring each module with its own tsconfig somewhat solves the problems mentioned above. But in this case, you need to compile each module separately, and since tsc is fundamentally built to spawn only one process, you can’t build based on multiple tsconfig files simultaneously or watch source code changes.

The TypeScript team tries to solve this problem with the references field.

// my-project/test/tsconfig.json
{
  "references": [
    // References my-project/src/tsconfig.json
    { "path": "../src" }
  ]
}

This example uses the references field to express that the test module references the src module. When the test module imports submodules from the src module, it imports the built output *.d.ts files rather than the source *.ts code. (Using tsc --build detects the module’s latest state and even does incremental builds.)

For more details about project references, check the official TypeScript Project References documentation, as it’s beyond the scope of this post.

Closing Thoughts

In this post, I examined the roles of several root-level fields in tsconfig. I wanted to cleanly organize tsconfig in a single post, but since this thing has such extensive options, I had no choice but to split it into multiple posts.

In the next post, I’ll discuss options related to type checking among TypeScript’s compiler options.

That concludes this post: [Everything About tsconfig] Root fields.

관련 포스팅 보러가기

Aug 22, 2021

[Everything About tsconfig] Compiler Options / Modules

Programming/Tutorial/JavaScript
Aug 08, 2021

[Everything About tsconfig] Compiler Options / Type Checking

Programming/Tutorial/JavaScript
Feb 07, 2026

Beyond Functors, All the Way to Monads

Programming
Jan 25, 2026

Why Do Type Systems Behave Like Proofs?

Programming
Nov 28, 2020

Designing Extensible Components with TypeScript

Programming/Architecture/JavaScript