The Complete tsconfig.json Guide: TypeScript Compiler Options for Modern Projects
TypeScripttsconfigCompilerConfigurationDeveloper Tools

The Complete tsconfig.json Guide: TypeScript Compiler Options for Modern Projects

TTypeScript Toolkit Editorial Team
2026-08-07
7 min read

Understand the TypeScript compiler options that matter most, with practical tsconfig.json starters for frontend, Node.js, and library projects.

A well-designed tsconfig.json makes TypeScript predictable: it defines which JavaScript features your code may use, how modules are resolved, how strictly types are checked, and what build artifacts are emitted. This guide explains the compiler options that matter most, then provides practical starter configurations for frontend applications, Node.js services, and reusable libraries.

Overview

tsconfig.json is the project configuration file read by the TypeScript compiler. Instead of passing a long list of flags to every tsc command, you place the project’s assumptions in one version-controlled file. Editors, linters, test tools, and build tools can also use this configuration directly or derive settings from it.

A useful configuration answers four questions:

  • What code is being checked? Use include, exclude, or project references to define the boundaries.
  • What environment will run the output? Use target, lib, and module to describe the runtime and module format.
  • How much type safety is required? Use strict and related checks to control the compiler’s level of scrutiny.
  • What should the compiler produce? Use outDir, declaration, sourceMap, and related options to control output.

There is no universal best tsconfig.json. A browser application, a Node.js service, and a published package have different runtime and distribution requirements. Start with the smallest configuration that accurately describes the project, then add an option when it solves a specific problem.

Core framework: the compiler options that shape a project

Strictness and type checking

For new projects, "strict": true is a sensible baseline. It enables a group of stricter checks, including protection against accidentally treating possibly missing values as definitely present. This often exposes more errors during development, but those errors are usually easier to fix before code reaches production.

If you are migrating an existing JavaScript codebase, enable strictness deliberately rather than changing every rule at once. You can begin with allowJs, checkJs, and a narrower set of checks, then tighten the configuration as modules are converted. Avoid disabling strictness globally just to make a migration compile; local annotations or carefully reviewed boundaries are usually more maintainable.

Options such as noUncheckedIndexedAccess and exactOptionalPropertyTypes can provide additional precision. They are valuable when a project handles configuration, API responses, or dictionary-like objects, but they may require changes to established code. Treat them as intentional policy decisions rather than automatic additions.

Target, lib, and module

target controls the JavaScript language level emitted by TypeScript. Choose it based on the environments that execute the output, not on the TypeScript version installed in the project. A frontend toolchain may transform the output further, while a Node.js service may run code with less transformation. If another tool performs bundling or transpilation, keep the responsibilities clear so that TypeScript is primarily checking types unless your build requires it to emit JavaScript.

lib describes the built-in APIs available to the type checker, such as DOM APIs or modern JavaScript collections. A browser application generally needs DOM-related libraries; a server project may not. Including a library that does not exist at runtime can hide mistakes, so align this setting with the actual environment.

module describes the module format used for imports and exports. The correct value depends on the runtime, package metadata, bundler, and whether the project uses ECMAScript modules or CommonJS. When imports fail despite apparently correct code, inspect module, moduleResolution, package boundaries, and the runtime’s module rules together. The related guide on fixing TypeScript module resolution errors is useful when this boundary is unclear.

Input, output, and build boundaries

Use rootDir to describe the source root and outDir to keep generated files separate from source files. A common arrangement is src for inputs and dist for output. Keep generated directories out of the compiler’s input set to prevent accidental reprocessing.

sourceMap creates mappings that let debuggers show the original TypeScript source instead of only emitted JavaScript. It is particularly useful for backend debugging and browser development. Decide separately whether source maps should be distributed with a published package.

For libraries, declaration: true emits .d.ts files so consumers can type-check imports. declarationMap can make editor navigation more useful by connecting declarations back to source files. Applications usually do not need to publish declaration files for their internal modules.

Paths and module aliases

baseUrl and paths can replace long relative imports with aliases such as @/components/Button. These options affect TypeScript’s understanding of imports; they do not automatically teach every runtime, test runner, bundler, or deployment tool how to resolve the alias.

Before adding an alias, confirm that the application bundler, test environment, editor, and production runtime share the same mapping. If only tsc understands the alias, the project may pass type checking and still fail at runtime.

Project scope and interoperability

include makes the checked files explicit, while exclude prevents common generated or dependency directories from entering the project. Do not use exclude as a substitute for a clear source layout; imported files can still become part of the program.

Options such as esModuleInterop and allowSyntheticDefaultImports can make imports from older CommonJS packages easier to use. Enable them only with an understanding of the module format emitted and the runtime behavior expected by the project. For larger codebases, shared base configurations and project references can reduce duplication; see the project references guide for a build-oriented approach.

Practical examples: starter configurations by project type

These examples are starting points, not drop-in replacements for every framework. Confirm the expectations of your bundler, test runner, deployment target, and package manager.

Frontend application

{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "jsx": "react-jsx",
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true,
    "baseUrl": ".",
    "paths": { "@/*": ["src/*"] }
  },
  "include": ["src"]
}

noEmit is appropriate when a separate frontend tool performs bundling and transformation. The alias must also be configured in that tool. For framework-specific patterns, compare this baseline with the project’s generated configuration rather than overwriting framework-managed settings. The Next.js TypeScript guide covers that distinction for an App Router project.

Node.js service

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": ["ES2022"],
    "rootDir": "src",
    "outDir": "dist",
    "strict": true,
    "sourceMap": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

For Node.js, module settings must agree with the package metadata and the way the service is started. Test the emitted files in the same module mode used in deployment. A conventional Express project can use this structure, but its request and response types still need deliberate modeling; configuration alone does not make an API type-safe.

Reusable library

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "rootDir": "src",
    "outDir": "dist",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "esModuleInterop": true
  },
  "include": ["src"]
}

A library also needs package metadata that points consumers to the intended JavaScript and declaration entry points. If a bundler creates the final package, decide whether TypeScript should emit declarations only or whether another build step owns all output. The comparison of TypeScript build tools can help clarify that division of responsibility.

Common mistakes and practical fixes

  • Copying a configuration without checking the runtime. A browser, Node.js process, and package consumer do not share the same built-in APIs or module behavior. Start from the runtime.
  • Assuming target polyfills APIs. It changes emitted syntax; it does not automatically provide missing runtime APIs. Use an appropriate runtime or polyfill strategy when required.
  • Using paths without configuring the build. Make aliases consistent across TypeScript, the bundler, tests, and deployment.
  • Hiding errors with broad exclusions. Excluding a folder does not necessarily remove imported files from the program. Fix project boundaries and imports instead.
  • Setting skipLibCheck without understanding the trade-off. It can reduce noise from declaration files, but it may also leave dependency type conflicts undiscovered. Keep it as a deliberate build-speed choice.
  • Changing several strictness flags at once during migration. Smaller changes make errors easier to classify. For common type failures, use targeted explanations such as the guide to string assignment errors.
  • Confusing compiler settings with application validation. TypeScript checks static assumptions; it does not validate untrusted JSON, forms, or network responses at runtime. Pair configuration with explicit parsing and validation where needed.

When to revisit your tsconfig.json

Review the configuration whenever the project’s execution or distribution model changes: moving from a bundler to direct Node.js execution, adding server-side rendering, introducing a worker, publishing a package, or adopting a new test runner. A new TypeScript version may also introduce compiler options or improved defaults worth evaluating, but do not change settings solely because they are available.

Use a short review checklist:

  1. Run tsc --showConfig to inspect the effective configuration, including inherited settings.
  2. Run the project’s type check and build in a clean environment.
  3. Verify that emitted modules actually run in the target environment.
  4. Check that source maps, declarations, and output directories match the release process.
  5. Test path aliases through the editor, test runner, bundler, and production command.
  6. Document unusual options so the next migration does not treat them as unexplained boilerplate.

Keep the file intentionally small, commit it with the project, and prefer a clear reason for every non-default option. When several related packages share the same rules, use a base configuration and extend it with project-specific settings. This makes compiler behavior easier to review while preserving the differences between applications, services, and libraries.

Related Topics

#TypeScript#tsconfig#Compiler#Configuration#Developer Tools
T

TypeScript Toolkit Editorial Team

Technical Editor

Senior editor and content strategist. Writing about technology, design, and the future of digital media. Follow along for deep dives into the industry's moving parts.