Skip to content
Home
TypeScript tsconfig.json: Compiler Options and Strict Mode Guide

TypeScript tsconfig.json: Compiler Options and Strict Mode Guide

TypeScript TypeScript 7 min read 1433 words Beginner ExcellentWiki Editorial Team

TypeScript’s compiler configuration is managed through tsconfig.json, a JSON file that controls everything from ECMAScript target to module resolution strategy, strictness level, and output formatting. Proper configuration prevents subtle bugs, optimizes build performance, and tailors the type checker to your project’s needs. Microsoft maintains comprehensive documentation on every compiler option (see TypeScript tsconfig Reference). This guide covers essential options, strict mode internals, project references, and advanced tuning.

Understanding tsconfig.json

Every TypeScript project starts with tsconfig.json. Generate one via npx tsc --init. The file supports compilerOptions, include, exclude, files, references, and TypeScript 5.x’s watchOptions.

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "strict": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
---

The compiler resolves tsconfig.json hierarchically — options from nested configs extend parent configs unless root is explicitly set.

Target and Module Settings

target

The target option specifies the ECMAScript version of the emitted JavaScript. Higher targets produce smaller, faster code but may not run in older environments.

TargetES VersionKey Features
ES5Wide compatDownlevel iteration, no Map/Set
ES2015ES6class, arrow functions, let/const
ES2020ES11Optional chaining, nullish coalescing
ES2022ES13Class fields, at() on arrays, error cause
ESNextLatestAlways targets the newest spec

For Node.js 20+, target ES2022 or ESNext. For browser libraries, target ES2015 or ES2020 depending on support requirements.

module

Controls the module code generation. Choices include:

  • ESNext: Preserves import/export — required for tree-shaking bundlers (Webpack, Rollup, esbuild).
  • NodeNext: Node.js native ESM — resolves .js extensions in imports.
  • CommonJS: require/module.exports — legacy but still used in many Node.js projects.
  • Preserve: Keeps original TypeScript import syntax — useful for transpilers like Babel or SWC that handle module resolution.

Strict Mode In Depth

strict: true enables a family of type-checking flags that collectively catch the largest class of real-world type errors.

strictNullChecks

The most impactful flag. Without it, null and undefined are assignable to any type. With it, they must be handled explicitly:

const name: string = null; // Error: Type 'null' is not assignable to 'string'

This flag alone prevents the “cannot read properties of null” class of runtime errors. In large codebases, enabling strictNullChecks can uncover hundreds of latent null-safety issues. The recommended migration strategy is to enable it early and fix all errors before adding other strict flags.

noImplicitAny

Forbids the compiler from inferring any when it cannot determine a type. Variables and parameters without explicit types or sufficient inference produce an error:

function log(value) { // Error: parameter implicitly has 'any' type
  console.log(value);
---

The exception is when the value is truly untyped, like deserialized JSON. In those cases, explicitly annotate unknown instead of any to force narrowing before use.

strictFunctionTypes

Enables stricter checking of function parameter bivariance. Without it, a function accepting Animal[] is assignable to one expecting Dog[]. With it, the compiler flags such assignments as unsound. This flag is particularly important for callback-heavy code — event emitters, array methods like forEach and map, and Promise chains all benefit from correct variance checking.

strictBindCallApply

Enables correct typing for Function.prototype.bind, call, and apply. Without it, these methods accept any arguments. With it, they are type-checked against the function signature.

strictPropertyInitialization

Ensures class properties are initialized in the constructor or via a default value. This prevents accessing uninitialized properties at runtime.

noUnusedLocals and noUnusedParameters

While not part of the strict bundle, these flags catch dead code. Combined with strict, they enforce a high bar for code quality.

{
  "compilerOptions": {
    "noUnusedLocals": true,
    "noUnusedParameters": true
  }
---

These flags prevent accumulation of dead code in growing codebases. If you use them, add // @ts-ignore annotations sparingly for intentional unused parameters (e.g., callback signatures that require unused arguments for API compatibility).

exactOptionalPropertyTypes

This flag (not part of strict but highly recommended) distinguishes between { prop?: string } and { prop?: string | undefined }. With exactOptionalPropertyTypes, writing obj.prop = undefined on an optional property produces a type error — optional means “not present,” not “present but undefined.” This aligns TypeScript behavior with the structure of JSON serialization and API contracts.

Advanced Compiler Options

noPropertyAccessFromIndexSignature

When enabled, accessing a property via dot notation (obj.prop) on a type with an index signature requires an explicit string index access (obj['prop']). This enforces API boundaries where dynamic keys should be accessed explicitly:

interface ApiResponse {
  [key: string]: unknown;
  status: 'ok' | 'error';
---

// With the flag enabled:
response.status;      // OK — known property
response.unknownProp; // Error — must use ['unknownProp']

isolatedModules: true

Ensures each file can be transpiled independently by non-tsc transpilers (Babel, SWC, esbuild). Disallows features that require cross-file type information, such as const enum exports and ambient namespaces. Enable this flag when using esbuild, SWC, or Babel as your transpiler to catch incompatibilities early.

Project References for Monorepos

Project references split a codebase into smaller TypeScript projects, each with its own tsconfig.json. They are ideal for monorepo architectures:

// packages/utils/tsconfig.json
{
  "compilerOptions": {
    "composite": true,
    "outDir": "./dist",
    "rootDir": "./src"
  }
---

The root tsconfig.json coordinates them:

{
  "references": [
    { "path": "./packages/utils" },
    { "path": "./packages/core" },
    { "path": "./apps/web" }
  ]
---

Benefits include faster incremental builds (only changed projects recompile), clear dependency ordering (TypeScript validates references), and parallel compilation across projects.

FAQ

What is the recommended target for a new project in 2026?
ES2022 for most projects. If you need to support IE11 (increasingly rare), target ES5. Node.js 22+ users can target ESNext for the latest runtime features.

Should I enable strict from the start?
Yes. Enabling strict on an existing codebase can require fixing hundreds of errors, but starting a new project with strict: true costs nothing and prevents entire categories of bugs. The TypeScript team explicitly recommends starting with strict.

How do I debug tsconfig resolution?
Run npx tsc --showConfig to see the resolved configuration after all extends are applied. Use npx tsc --traceResolution to debug module resolution issues and understand why a particular import resolves to one file over another.

What is the difference between moduleResolution: "node" and "nodenext"?
node emulates classic Node.js resolution (no extension detection for ESM). nodenext follows Node.js’s modern ESM resolution — you must include .js extensions in relative imports when outputting ESM.

How do I configure path aliases for both TypeScript and my bundler?
Set paths in tsconfig for the TypeScript compiler, and mirror the same aliases in your bundler’s config (resolve.alias in Webpack, alias in Vite/esbuild). Tools like tsconfig-paths and tsc-alias can synchronize them.

Performance and Build Time Optimization

TypeScript compilation time increases with codebase size. These strategies keep builds fast:

  • skipLibCheck: true — Skips type-checking of .d.ts files in node_modules. This is the single biggest build time optimization, often reducing time by 50-70%.
  • incremental: true — Enables incremental compilation using .tsbuildinfo files. Subsequent builds only re-process changed files.
  • Project references — Splitting a monorepo into composite projects enables parallel compilation. Each project compiles independently, and only changed projects require recompilation.
  • isolatedModules: true — Allows transpilers like esbuild and SWC to process each file independently, avoiding cross-file type resolution during transpilation.

For development workflows, use tsc --noEmit in a background terminal for type-checking, and a fast transpiler (esbuild, SWC) for output generation. This gives you type safety without waiting for full tsc output.

TypeScript 5.x Configuration Highlights

Recent TypeScript versions have added useful compiler options:

  • moduleResolution: "bundler" (TS 5.0): Optimized for bundler environments like Vite, Webpack, and Rollup. It relaxes some Node.js ESM restrictions while maintaining strict module resolution.
  • allowImportingTsExtensions (TS 5.0): Allows .ts extensions in import paths when using bundler-based projects. This simplifies migration when tooling handles extension stripping.
  • customConditions (TS 5.x): Adds custom conditions for exports field resolution in package.json, useful for framework authors who need conditional module resolution.
  • resolvePackageJsonImports / resolvePackageJsonExports (TS 5.x): Controls whether TypeScript follows the exports and imports fields in package.json, providing stricter module boundary enforcement.

These options make TypeScript’s configuration model increasingly flexible for modern bundler-based workflows while maintaining compatibility with traditional Node.js environments.

Migrating Configuration Between Projects

When starting a new TypeScript project, you can base your configuration on known-good setups:

  • Library: target: "ES2022", module: "ESNext", declaration: true, declarationMap: true, sourceMap: true
  • Node.js application: target: "ES2022", module: "NodeNext", outDir: "./dist", rootDir: "./src"
  • Browser application (Vite): target: "ES2020", module: "ESNext", moduleResolution: "bundler", jsx: "react-jsx"
  • Monorepo package: composite: true, declaration: true, declarationMap: true, references to sibling packages

The TypeScript blog and release notes regularly publish recommended configuration patterns for each new version. Checking these before starting a new project ensures you benefit from the latest compiler improvements.

Internal Links

References

Section: TypeScript 1433 words 7 min read Beginner 756 articles in section Report inaccuracy Back to top