TypeScript tsconfig.json: Compiler Options and Strict Mode Guide
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.
| Target | ES Version | Key Features |
|---|---|---|
ES5 | Wide compat | Downlevel iteration, no Map/Set |
ES2015 | ES6 | class, arrow functions, let/const |
ES2020 | ES11 | Optional chaining, nullish coalescing |
ES2022 | ES13 | Class fields, at() on arrays, error cause |
ESNext | Latest | Always 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: Preservesimport/export— required for tree-shaking bundlers (Webpack, Rollup, esbuild).NodeNext: Node.js native ESM — resolves.jsextensions 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.tsfiles innode_modules. This is the single biggest build time optimization, often reducing time by 50-70%.incremental: true— Enables incremental compilation using.tsbuildinfofiles. 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.tsextensions in import paths when using bundler-based projects. This simplifies migration when tooling handles extension stripping.customConditions(TS 5.x): Adds custom conditions forexportsfield resolution in package.json, useful for framework authors who need conditional module resolution.resolvePackageJsonImports/resolvePackageJsonExports(TS 5.x): Controls whether TypeScript follows theexportsandimportsfields 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.