tsconfig & Compiler Options

10 questions found

tsconfig.json marks a directory as the root of a TypeScript project and configures how the compiler behaves — 'compilerOptions' specifically holds settings controlling type-checking strictness, the JavaScript output target/format, module resolution, and where compiled files are written.
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "outDir": "./dist",
    "strict": true
  },
  "include": ["src/**/*"]
}
Real-world example Setting up a new TypeScript project's baseline configuration for compiling and type-checking source files.

Common follow-ups: What happens if you run 'tsc' in a directory with no tsconfig.json present at all?

Modules
'target' specifies which ECMAScript version the compiled JavaScript output should conform to — newer language features (like optional chaining, async/await, or classes) get automatically transformed ('downleveled') into older equivalent syntax if the target doesn't natively support them, ensuring compatibility with older runtimes.
// target: 'ES5' downlevels arrow functions, classes, async/await, etc. into ES5-compatible code
// target: 'ES2020' keeps most modern syntax as-is, assuming a modern runtime
Real-world example Choosing 'ES2020' or newer for a modern browser/Node.js target versus 'ES5' for supporting very old browsers like IE11.

Common follow-ups: Does a higher 'target' setting affect TYPE CHECKING strictness, or only the shape of the emitted JavaScript?

Modules
'strict': true is a shorthand that enables a whole family of individual strictness flags at once, including noImplicitAny, strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, alwaysStrict, and useUnknownInCatchVariables — collectively representing TypeScript's recommended, most type-safe configuration.
// Equivalent to manually setting all of these to true:
{
  "compilerOptions": {
    "strict": true
    // implies: noImplicitAny, strictNullChecks, strictFunctionTypes,
    // strictBindCallApply, strictPropertyInitialization, noImplicitThis,
    // alwaysStrict, useUnknownInCatchVariables
  }
}
Real-world example Enabling the single 'strict' flag as the recommended baseline for any new TypeScript project, rather than manually toggling each sub-flag.

Common follow-ups: Can you enable 'strict': true globally but explicitly opt OUT of just one specific sub-flag?

Migrating JavaScript to TypeScript
'include' specifies glob patterns for files/directories to bring INTO the project (with 'exclude' subtracting specific patterns back out, commonly node_modules by default); 'files' instead lists an EXPLICIT, exhaustive array of individual file paths — useful for very small projects, but impractical to maintain manually as a project grows.
{
  "include": ["src/**/*.ts"],
  "exclude": ["src/**/*.test.ts", "node_modules"]
}
// vs.
{
  "files": ["src/index.ts", "src/utils.ts"] // must list every file explicitly
}
Real-world example Excluding test files or generated code from the main application's production TypeScript compilation.

Common follow-ups: What is included by default if NEITHER 'include' nor 'files' is specified at all?

Modules
Both flags flag declared-but-never-used local variables or function parameters as compile errors, helping catch dead code and leftover debugging variables; to intentionally allow an unused parameter (like a required positional parameter you don't need), prefix its name with an underscore, which the compiler specifically exempts from this check.
// tsconfig.json: { "noUnusedParameters": true }
function handler(event: Event, _index: number) {
  console.log(event); // '_index' is unused but exempted due to the underscore prefix
}
Real-world example Catching leftover debugging variables or genuinely dead code left behind during refactoring, enforced automatically in CI.

Common follow-ups: Does this underscore-prefix exemption apply to unused LOCAL VARIABLES too, or only function parameters?

Migrating JavaScript to TypeScript
Project references let you split a large codebase into independently-buildable sub-projects with explicit declared dependencies between them; TypeScript then performs INCREMENTAL builds (via 'tsc --build'), only recompiling projects whose dependencies actually changed, and enforces that a project can only import from packages it explicitly references — catching accidental circular or unauthorized cross-package imports at the project-structure level.
// packages/shared/tsconfig.json
{ "compilerOptions": { "composite": true }, "include": ["src"] }

// packages/app/tsconfig.json
{
  "compilerOptions": { "composite": true },
  "references": [{ "path": "../shared" }]
}
// Build with: tsc --build packages/app
Real-world example Structuring a large monorepo so changing one package doesn't force a full, slow rebuild of every unrelated package.

Common follow-ups: What must 'composite': true additionally require regarding the 'declaration' compiler option?

Modules
skipLibCheck skips type-checking of ALL .d.ts declaration files (including your own dependencies' bundled types), significantly speeding up compilation — especially in large projects with many dependencies — at the cost of not catching legitimate type errors or conflicts that might exist WITHIN those declaration files themselves (which is rare, but does occasionally happen with buggy or conflicting @types packages).
// tsconfig.json
{
  "compilerOptions": { "skipLibCheck": true } // commonly recommended default for faster builds
}
Real-world example Significantly speeding up compilation in a large project with many node_modules dependencies, at minimal practical risk.

Common follow-ups: Why is skipLibCheck considered a safe and commonly recommended default despite technically skipping some checking?

tsconfig & Compiler Options
'extends' points to a base tsconfig file whose compilerOptions are inherited as defaults; the extending file's own compilerOptions are then MERGED on top, with the extending file's values taking precedence for any overlapping keys — letting a monorepo maintain one shared 'tsconfig.base.json' while individual packages only specify their differences.
// tsconfig.base.json
{ "compilerOptions": { "strict": true, "target": "ES2020" } }

// packages/app/tsconfig.json
{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": { "outDir": "./dist" } // adds to, doesn't replace, the base config
}
Real-world example Maintaining consistent strictness and target settings across dozens of packages in a monorepo from one shared base file.

Common follow-ups: Do array-valued options like 'include'/'exclude' MERGE across extends, or does the extending file's array fully REPLACE the base's?

tsconfig & Compiler Options
With 'incremental': true, TypeScript writes a .tsbuildinfo file caching information about the previous compilation; on subsequent runs, it uses this cache to determine which files actually need to be reprocessed based on what changed, rather than re-type-checking the entire project from scratch every single time — dramatically speeding up repeated compiles in watch mode or CI.
// tsconfig.json
{
  "compilerOptions": {
    "incremental": true,
    "tsBuildInfoFile": "./.cache/tsbuildinfo"
  }
}
Real-world example Speeding up a large project's repeated local 'tsc --watch' compilations, or caching build state between CI pipeline runs.

Common follow-ups: Should the generated .tsbuildinfo file typically be committed to version control, or added to .gitignore?

Modules
A published library should set 'declaration': true (so consumers get type information) and typically 'declarationMap': true plus 'sourceMap': true (so consumers' editors can jump to real source and debug through it); an internal application typically doesn't need 'declaration' output at all, since nothing downstream consumes it as a typed dependency — reducing unnecessary build output.
// Library tsconfig.json
{
  "compilerOptions": {
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "dist"
  }
}
// Internal app tsconfig.json typically omits 'declaration' entirely
Real-world example Configuring a monorepo where shared internal packages generate full declaration output for consumption by other packages, while the top-level app doesn't.

Common follow-ups: Why would generating unnecessary .d.ts files for a non-published internal application be considered pure build overhead?

tsconfig & Compiler Options