Command Palette

Search for a command to run...

Hectal
PHASE 12Intermediate ~23 min· topic 1 of 6

Topic 12.1

tsconfig.json from Top to Bottom

In one line

A tsconfig.json is the settings file the compiler reads: which files belong to the project, which rules to check them with, and what to write out. Reading it fluently is the base for everything else in this phase.

Think of it like this

A school exam has a cover sheet. It lists which pupils sit the exam (the class list), which rules apply (calculators allowed or not) and where the finished papers go. The questions are your code; the cover sheet is tsconfig.json. Two schools can set the same questions and mark them very differently, just by changing the cover sheet.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

tsconfig.json
The settings file for a TypeScript project: which files are in it and which compiler options apply.
compilerOptions
The section of tsconfig.json with the rules and output settings, such as strict, target and outDir.
Project root
The folder that contains the tsconfig.json. Relative paths and the default rootDir start here.
Glob pattern
A path with wildcards. * matches part of a name, matches any number of folders. "src//*.ts" means every .ts file anywhere under src.
include / exclude / files
The three keys that choose which files are compiled. files is an exact list, include is patterns, exclude removes matches from include.
extends
Builds one config on top of another. Options are merged, but the file lists are replaced if you write them.
Default
The value an option has when you don't write it. TypeScript 6.0 changed several, for example strict is now true.
--showConfig
A tsc flag that prints the final merged configuration, so you can see exactly what the compiler will use.
--noEmit
Check types but write no output files.

Step by step

01Make the config with tsc --init

Start in an empty project folder and run tsc --init. It writes tsconfig.json. Here is the real file from TypeScript 7.0.2, trimmed. Lines starting with // are comments and do nothing.

Read it in five groups: file layout (rootDir, outDir, still commented out), environment (module, target, types), other outputs (source maps and declaration files), stricter checks and style options, then the recommended block.

tsconfig.jsonwhole filejson
{
  "compilerOptions": {
    // File Layout
    // "rootDir": "./src",
    // "outDir": "./dist",

    // Environment Settings
    "module": "nodenext",
    "target": "esnext",
    "types": [],

    // Other Outputs
    "sourceMap": true,
    "declaration": true,
    "declarationMap": true,

    // Stricter Typechecking Options
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,

    // Style Options (all commented out) ...
    // "noImplicitReturns": true,
    // "noFallthroughCasesInSwitch": true,

    // Recommended Options
    "strict": true,
    "jsx": "react-jsx",
    "verbatimModuleSyntax": true,
    "isolatedModules": true,
    "noUncheckedSideEffectImports": true,
    "moduleDetection": "force",
    "skipLibCheck": true,
  }
}

02What the recommended block means

verbatimModuleSyntax makes import and export lines stay exactly as you wrote them: a type-only import must say import type, so what disappears at build time is always visible in the source. isolatedModules makes TypeScript warn about code that a tool compiling one file at a time (esbuild, Babel) could not handle. moduleDetection: "force" treats every file as a module, so two files can both declare const name without clashing.

noUncheckedSideEffectImports checks that import "./polyfill" points at a real file. skipLibCheck skips type-checking inside .d.ts files (yours included), which saves time and hides errors from other people's packages (Topic 11.4). jsx only matters for .tsx files (Phase 14).

03Choose the files

Now a small project: src/index.ts, src/index.test.ts and tests/other.ts, plus a folder node_modules. A shared base file holds the rules; the project file extends it and picks the files.

tsconfig.base.json and tsconfig.jsonwhole filejson
// tsconfig.base.json
{
  "compilerOptions": {
    "strict": true,
    "target": "es2022",
    "module": "esnext",
    "moduleResolution": "bundler",
    "outDir": "dist",
    "rootDir": "src"
  }
}

// tsconfig.json
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": { "noImplicitReturns": true },
  "include": ["src"],
  "exclude": ["src/**/*.test.ts"]
}

04Ask the compiler what it sees

tsc --showConfig prints the merged result. Notice what the compiler added: the files list it found, and ./dist written as a path. noImplicitReturns from the child and everything from the base are in one object.

terminalwhole filebash
$ tsc --showConfig
{
    "compilerOptions": {
        "module": "esnext",
        "moduleResolution": "bundler",
        "noImplicitReturns": true,
        "outDir": "./dist",
        "rootDir": "./src",
        "strict": true,
        "target": "es2022"
    },
    "files": [
        "./src/index.ts"
    ],
    "include": [
        "src"
    ],
    "exclude": [
        "src/**/*.test.ts"
    ]
}

05Which files win: files, include, exclude

Add "files": ["tests/other.ts"] and also "exclude": ["tests", ...]. --listFilesOnly (it also lists the standard library files, here removed with grep) shows tests/other.ts is still in. A path in files is never filtered by exclude.

terminalwhole filebash
$ tsc -p tsconfig.files.json --listFilesOnly | grep -v typescript-win32
.../cfg/tests/other.ts
.../cfg/src/index.ts

# No include/files at all: every .ts file below the config, except node_modules and dist
$ tsc -p tsconfig.all.json --listFilesOnly | grep -v typescript-win32
.../cfg/src/index.test.ts
.../cfg/src/index.ts
.../cfg/tests/other.ts

06The file-names-plus-tsconfig trap

You are in the same folder and type tsc src/index.ts to check one file. The compiler stops instead of ignoring your config.

Pick on purpose: tsc -p . to use the config, or tsc --ignoreConfig src/index.ts to compile just this file with default options.

terminalwhole filebash
$ tsc src/index.ts
error TS5112: tsconfig.json is present but will not be loaded if files are specified on commandline. Use '--ignoreConfig' to skip this error.
The file-names-plus-tsconfig trapdiagram
Rendering diagram…

Try it yourself

  1. 1

    Predict the flag

    In the first example, add a type to the parameter: function double(n: number). Predict the Types panel before you look.

    It says no type errors. A flag like noImplicitAny never complains about code that already says what it means.

  2. 2

    Read a config

    Open the tsc --init file above. For each line outside the comments, say in one sentence what bug or surprise it prevents. Then look at which lines are commented out and ask whether your project would want them (Topic 12.2 covers the style options).

  3. 3

    Predict the file list

    In the project with include: ["src"] and exclude: ["src/**/*.test.ts"], you add src/helper.test.ts and import it from src/index.ts. Is it compiled? Yes: exclude only trims the starting list; an import pulls the file back in.

Code & diagrams

strict includes noImplicitAny: an untyped parameter is an error

Every live example uses the options from tsc --init, so strict is on. Read the error, then look at the next example: the same code with one flag switched off.

Console

(nothing was printed)

Expected console output

8
The same code with noImplicitAny switched off

This example's tsconfig overrides one option: noImplicitAny is false. The error is gone because n is silently typed any, which switches checking off for it. Nothing about the emitted JavaScript changed.

Console

(nothing was printed)

Expected console output

8
allowUnreachableCode: false turns a hint into an error

By default (option not set) an editor greys out unreachable code but tsc is silent. Setting the option to false makes it a real error you can fail a build on. Set it to true to hide even the editor hint.

Console

(nothing was printed)

Expected console output

positive

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Give tsc a file and a tsconfig

In a folder that has a tsconfig.json, run tsc src/index.ts instead of tsc.

terminal
$ tsc src/index.ts
── what you'll see ──
error TS5112: tsconfig.json is present but will not be loaded if files are specified on commandline. Use '--ignoreConfig' to skip this error.

Break #2

Trust a stale mental model of the config

Add "files": ["tests/other.ts"] while "exclude" also lists "tests", and expect the file to be skipped.

terminal
$ tsc -p tsconfig.files.json --listFilesOnly
── what you'll see ──
.../cfg/tests/other.ts

Myth vs fact

Myth

exclude stops TypeScript from ever looking at a file.

Fact

It only removes files from the starting list that include built. Anything imported from an included file is still read and checked.

Myth

A child config merges its include with the parent's.

Fact

compilerOptions are merged. files, include and exclude from the child replace the parent's lists completely.

Myth

strict is off unless you turn it on.

Fact

In TypeScript 6.0 and 7.0 it defaults to true. Without a tsconfig, tsc file.ts also gets the new defaults.

Myth

tsconfig.json must be valid JSON.

Fact

Comments and trailing commas are allowed; the compiler reads it with a more forgiving parser. Other tools that read it as plain JSON may not.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Editors resolve a file's config by walking up from the file. In a repo with several tsconfig files, a file that no config includes falls into an "inferred project" (the editor's fallback for loose files, with its own default options and none of your config), which is why a file can show different errors in the editor and in the build. Make sure every source file is covered by exactly one config (a root tsconfig.json with references, Topic 12.4, is the usual answer).

  • ▸

    extends can point at a package name as well as a file path, so a team can publish its base settings on npm and every project inherits them. Read such a package's own documentation for its current options instead of copying them blindly, because bases written for older compilers may set options that 6.0 deprecated and 7.0 removed (Topic 12.3).

  • ▸

    In 7.0 the compiler is native code, but the config format is unchanged, and options removed in 6.0/7.0 produce explicit errors (TS5108, Topic 12.3) instead of being ignored. Run tsc --showConfig in CI once after an upgrade and diff it: it shows defaults you inherited without writing them.

Remember this

  1. 1

    The problem this solves: the compiler needs to know which files belong to your project and which rules to check them with, and typing that on every run would be slow and easy to get wrong. A tsconfig.json is a JSON file in the root folder of a project. When you run tsc with no file names, the compiler looks for tsconfig.json in the current folder and then in each parent folder, and uses the first one it finds. The folder that holds it is the project root. Editors such as VS Code do the same search for every file you open, which is why a missing config makes your editor behave differently from your build. tsconfig files are allowed to contain // comments and trailing commas, even though plain JSON is not.

  2. 2

    **compilerOptions** holds the rules and output settings: strict, target, module, outDir and about a hundred more. Anything you leave out gets a default. TypeScript 6.0 changed several defaults, and 7.0 keeps them: strict is true, module is esnext, types is [] (no global @types packages are loaded unless you list them), and rootDir is the folder of the tsconfig itself. If you read an older tutorial that says "strict is off by default", it is describing the old compiler.

  3. 3

    Which files are in the project is decided by three keys. files is an exact list of paths. include is a list of glob patterns (patterns with wildcards: * matches any name part, ** matches any folders in between) such as "src" or "src/**/*". exclude removes files from what include matched. If you write neither files nor include, the default is include: ["**/*"] (every .ts, .tsx and .d.ts below the config). exclude defaults to node_modules and the outDir, so your own compiled output and your dependencies are never re-compiled by accident. The set of files the compiler ends up reading (the ones listed plus everything they import) is called the program. Two traps: exclude only filters the starting list, so a file that something in the project imports is still compiled even if you excluded it; and a path in files always wins over exclude.

  4. 4

    **extends** lets one config build on another so you write shared rules once. Your tsconfig.json says "extends": "./tsconfig.base.json" and then lists only what differs. The rule is: compilerOptions are merged key by key (yours win), but files, include and exclude are replaced as a whole if you write them, not merged. Relative paths such as outDir are resolved from the folder of the file that wrote them, not from the file that extends it. extends may also be a list (later entries win) or the name of a package, so a team can publish its settings once. references (Topic 12.4) is never inherited.

  5. 5

    **tsc --init** writes a starter tsconfig.json with the options most projects want, and it is better than copy-pasting an old one. In TypeScript 7.0.2 it sets module: nodenext, target: esnext, types: [], source maps and declarations on, strict, and two extra checks that strict does not include (noUncheckedIndexedAccess, exactOptionalPropertyTypes). It also leaves the style flags and the folder settings commented out for you to choose. That generated file is what this course's live examples are modelled on, which is why they already have those two extra checks on.

  6. 6

    **tsc --showConfig** is the debugging tool for everything above. It prints the final, merged config the compiler will really use: after extends, with relative paths resolved and the real list of files that include and exclude matched. When a file is "not being checked" or an option "does nothing", run --showConfig and --listFilesOnly before you guess. Other handy flags: -p <path> (use a specific config), --noEmit (check only), and --ignoreConfig (ignore the tsconfig even if one exists).

  7. 7

    One more rule trips almost everyone once. If a tsconfig.json exists and you pass file names (tsc index.ts), the compiler refuses and prints error TS5112: tsconfig.json is present but will not be loaded if files are specified on commandline. Use '--ignoreConfig' to skip this error. Older versions silently dropped your config and compiled with defaults; 6.0 and 7.0 make you say which one you mean.

Explain it without notes

01

What does tsc do when you run it with no arguments?

02

Explain how include, exclude and files interact.

03

How does extends merge two configs?

04

Why does tsc index.ts fail in a folder that has a tsconfig.json, and what are your options?

05

How would you find out which settings are really in force for a project?

Practice

01

Create tsconfig.base.json with strict and target: es2022, and a tsconfig.json that extends it, includes only src, and excludes src/**/*.test.ts. Write the two files and describe what --showConfig prints for files.

02

A teammate says "I added tests to exclude but the tests still get type-checked in the build." Give two reasons that could be true and how you would check each.

Trade-offs

  • ↔

    One shared base config keeps teams consistent, but every project that extends it inherits changes the moment the base changes. Pin a published base to a version, and run --showConfig in CI to see drift.

  • ↔

    A broad include is convenient, but it can pull test files and scripts into your production build. A narrow include is explicit but easy to forget to extend when you add a folder.

Done when you can

  • I can explain how tsc finds and merges a tsconfig.json.

  • I can say what include, exclude and files do, and why an imported file ignores exclude.

  • I can read the tsc --init file and explain each option in a sentence.

  • I can use --showConfig and --listFilesOnly to debug which files and options apply.

  • I can explain the TS5112 error and the two ways out of it.