tsconfig.json and Compiler Options

This section documents the current TypeScript release line as published at the official TypeScript documentation, which is the reference these pages are written and verified against. No specific patch version is pinned.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, since TypeScript iterates quickly.

This section’s bibliography lists the reference material consulted while preparing these pages.

tsc reads its settings from a tsconfig.json file at the project root. That file marks the directory as the project; running tsc with no input files then compiles every file the config selects. The three companion references are the tsconfig reference (every option, one page), the tsconfig.json handbook (how the file works), and the compiler options handbook (the CLI flag list).

tsc --init and the config file

tsc --init writes a commented starter tsconfig.json:

npm install --save-dev typescript
npx tsc --init             # writes ./tsconfig.json
npx tsc                    # compile the project described by ./tsconfig.json
npx tsc -p ./packages/api  # compile using a specific tsconfig

Almost every option has a matching CLI flag (tsc --target es2022 --strict). Flags passed on the command line override the file for that run, but a project build (tsc -p, or a bare tsc that finds a tsconfig.json) ignores individual file arguments — you cannot mix tsc file.ts with a project. Use flags for one-off runs; use the file for anything repeatable.

files, include, exclude

{
  "compilerOptions": { "outDir": "dist" },
  "files": ["src/main.ts"],
  "include": ["src/**/*", "types/**/*.d.ts"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}

files is an explicit list (no globs). include / exclude take globs; exclude only removes entries that include added, and defaults to node_modules, bower_components, jspm_packages, and the outDir. If neither files nor include is present, the compiler takes every .ts / .tsx / .d.ts file under the config directory. A file pulled in by an import or a /// <reference> is compiled even when no pattern lists it.

extends and shared config bases

extends merges another config into this one; the deriving file wins on conflicts. Relative paths such as outDir in the base resolve relative to the base file, not the deriving one.

npm install --save-dev @tsconfig/node20 @tsconfig/strictest
{
  "extends": ["@tsconfig/node20/tsconfig.json", "@tsconfig/strictest/tsconfig.json"],
  "compilerOptions": {
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src"]
}

The community @tsconfig/* bases package a runtime target (@tsconfig/node20, @tsconfig/deno, @tsconfig/create-react-app) or a strictness level (@tsconfig/recommended, @tsconfig/strictest) so each project only writes what is genuinely project-specific. extends accepts a single string or an array; with an array, later entries win.

The ${configDir} substitution

Inside a base config, ${configDir} (TypeScript 5.5+) expands to the directory of the leaf project that is loading the base, not the directory of the base itself. It lets one shared base name paths that resolve against each consumer.

// tsconfig.base.json -- shared across a monorepo
{
  "compilerOptions": {
    "outDir": "${configDir}/dist",
    "rootDir": "${configDir}/src",
    "tsBuildInfoFile": "${configDir}/dist/.tsbuildinfo",
    "typeRoots": ["${configDir}/node_modules/@types", "${configDir}/types"]
  }
}

It is written ${configDir} in this prose only to stop AsciiDoc from treating it as an attribute reference; in the JSON file it is literal. Without it, a relative outDir in a base always resolves next to the base file, so every project sharing the base would emit into one folder.

strict and its members

strict is a bundle: turning it on enables every flag in the table below, and keeps enabling newly added ones in future releases. Turn the bundle on, then switch individual members off only with a reason. See strict.

{
  "compilerOptions": {
    "strict": true,

    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitOverride": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  }
}
Member enabled by strict Effect

noImplicitAny

Error when a parameter or variable is implicitly any.

strictNullChecks

null and undefined are not members of every type and must be handled explicitly.

strictFunctionTypes

Function-type parameter positions are checked contravariantly (method parameters excepted).

strictBindCallApply

bind, call, and apply are type-checked against the target signature.

strictPropertyInitialization

Class fields must be assigned in the constructor, or declared optional or with !.

useUnknownInCatchVariables

The catch binding is typed unknown instead of any.

noImplicitThis

Error on a this whose type cannot be determined.

alwaysStrict

Parse every file in strict mode and emit "use strict".

The linting-adjacent flags are not part of strict and are set on their own:

Flag Effect

noUnusedLocals

Error on an unused local variable.

noUnusedParameters

Error on an unused parameter; prefix the name with _ to exempt it.

noImplicitReturns

Every code path in a value-returning function must return.

noFallthroughCasesInSwitch

A non-empty case may not fall through to the next.

noUncheckedIndexedAccess

Index and element access add undefined to the result type.

exactOptionalPropertyTypes

? no longer implies an added undefined; assigning undefined explicitly becomes an error.

noImplicitOverride

A method that overrides a base method must carry the override keyword.

Emit options

These control what JavaScript tsc writes and how it is downleveled; the full list is on the tsconfig reference.

{
  "compilerOptions": {
    "target": "es2022",
    "lib": ["es2022", "dom", "dom.iterable"],
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "rootDir": "src",
    "outDir": "dist",

    "declaration": true,
    "sourceMap": true,
    // "inlineSourceMap": true,   // folds the map into the .js; cannot combine with sourceMap

    "removeComments": false,
    "downlevelIteration": true,
    "importHelpers": true,        // emit helpers as tslib imports; run: npm i tslib

    "jsx": "react-jsx",

    "verbatimModuleSyntax": true,
    "isolatedModules": true,

    "noEmit": false,
    "noEmitOnError": true
  }
}
  • target sets the output ECMAScript level and the default lib. lib overrides which built-in API declarations are in scope — add dom for browser globals.

  • module picks the output module format; moduleResolution picks how bare specifiers are looked up. nodenext / node16 follow Node’s ESM rules and want extensions on relative imports; bundler fits Vite / esbuild / webpack. Depth in Modules.

  • outDir / rootDir place emitted files. rootDir pins the base folder so the tree under outDir mirrors the sources.

  • declaration emits .d.ts; sourceMap emits .js.map; inlineSourceMap folds the map into the .js and is mutually exclusive with sourceMap.

  • noEmit type-checks only, leaving output to a bundler; noEmitOnError withholds all output when any error is found.

  • removeComments strips comments from output. downlevelIteration makes for…​of and spread correct on arbitrary iterables when target is below ES2015, at the cost of larger output. importHelpers pulls __extends-style helpers from tslib instead of inlining them into every file.

  • jsx selects JSX handling — react-jsx for the modern runtime, preserve to leave it for a bundler; see TypeScript with React.

  • verbatimModuleSyntax drops any import / export that referenced only types and leaves the rest exactly as written, so import type becomes mandatory for type-only imports. isolatedModules restricts you to syntax a single-file transpiler (Babel, esbuild) can process.

For single-file transpilers, TypeScript 5.8 adds the --erasableSyntaxOnly flag: it bans runtime-only constructs (enum, value-bearing namespace, parameter properties, import =) so each file is valid "types-as-comments" JavaScript.

npx tsc --erasableSyntaxOnly --noEmit

Type-checking configuration

{
  "compilerOptions": {
    "skipLibCheck": true,
    "types": ["node", "vitest/globals"],
    "typeRoots": ["./node_modules/@types", "./types"],
    "allowJs": true,
    "checkJs": false
  }
}
  • skipLibCheck skips type-checking of .d.ts files, including those in dependencies — a large speed-up and the common default. types restricts the auto-included @types/ packages to just the listed names (otherwise every package under node_modules/@types is global). typeRoots changes *where ambient type packages are searched.

  • allowJs lets .js files join the program and be emitted; checkJs additionally type-checks them, reading JSDoc as annotations — the usual first step of a migration, also covered in TypeScript with React.

Watch mode and inspection flags

npx tsc --watch                  # rebuild on change; alias -w
npx tsc --watch --preserveWatchOutput
npx tsc --showConfig             # print the fully resolved config (after extends) and exit
npx tsc --explainFiles           # list every file in the program and why it was included
npx tsc --noEmit --pretty false  # plain, uncoloured diagnostics for CI

--showConfig is how you see what extends and ${configDir} actually resolved to. --explainFiles answers "why is this file being compiled?". --pretty is on by default and controls colour and formatting of diagnostics. Under --watch, assumeChangesOnlyAffectDirectlyDependentFiles makes a rebuild re-check only direct importers instead of walking the whole dependency graph — faster, occasionally less accurate.

{
  "compilerOptions": {
    "assumeChangesOnlyAffectDirectlyDependentFiles": true
  },
  "watchOptions": {
    "watchFile": "useFsEvents",
    "excludeDirectories": ["**/node_modules", "dist"]
  }
}

See also