Skip to main content
C
CodeUtil

My tsconfig.json After Years of TypeScript (Copy This)

I've tweaked my TypeScript config hundreds of times. Here's what I actually use now after years of trial and error on real projects.

2025-07-2418 min
Related toolJSON Formatter

Use the tool alongside this guide for hands-on practice.

Why I obsess over tsconfig.json

I've probably spent more time tweaking tsconfig.json than I'd like to admit. But here's the thing: getting it right once saves you countless hours of debugging weird TypeScript behavior. I've seen projects where developers fight with their IDE, miss obvious bugs, and blame TypeScript itself - when the real culprit was a misconfigured tsconfig.

At Šikulovi s.r.o., I've settled on a setup that works for web apps, Node.js backends, and libraries. It's strict enough to catch real bugs but not so strict that you're fighting the type system all day. I'm sharing exactly what I use and why.

This isn't theoretical - it's what I run in production on CodeUtil and client projects. Use the JSON Formatter to validate your tsconfig.json syntax if you're making changes.

tsconfig.json structure explained

The tsconfig.json file has a consistent structure with several top-level properties. Understanding this structure helps you navigate the many available options.

The compilerOptions object contains most configuration—type checking rules, module settings, output options, and path mappings. This is where you spend most of your configuration time.

The include and exclude arrays control which files TypeScript processes. By default, TypeScript includes all .ts and .tsx files in the project directory. Use these arrays to focus on source files and exclude build outputs, node_modules, and test files as needed.

The extends property allows configuration inheritance. Base configurations from packages like @tsconfig/recommended provide sensible defaults that you can customize. This reduces duplication across projects and ensures consistency.

  • compilerOptions: Type checking, module resolution, output settings
  • include: Glob patterns for files to compile
  • exclude: Glob patterns for files to skip
  • extends: Inherit from base configurations
  • files: Explicit list of files (rarely used)
  • references: Project references for monorepos

Compiler options deep dive

Compiler options control how TypeScript checks and transforms your code. They fall into several categories: type checking, modules, emit, and interoperability.

Type checking options like strict, noImplicitAny, and strictNullChecks determine how thoroughly TypeScript analyzes your code. These options catch bugs but require more explicit type annotations.

Module options like module, moduleResolution, and esModuleInterop control how TypeScript understands imports and exports. The right settings depend on your target environment—Node.js, browsers, or bundlers.

Emit options like target, outDir, and declaration control the JavaScript output. They determine which ECMAScript version to target, where to write compiled files, and whether to generate type declaration files.

  • target: ECMAScript version for output (ES2020, ES2022, ESNext)
  • module: Module system (CommonJS, ESNext, NodeNext)
  • moduleResolution: How to find modules (Node, Bundler, NodeNext)
  • lib: Available type definitions (DOM, ES2020, ESNext)
  • outDir: Directory for compiled JavaScript
  • rootDir: Root directory for source files
  • declaration: Generate .d.ts type declaration files

Strict mode: when and why

Strict mode enables a collection of type checking options that catch common errors. The strict flag is a shorthand that enables multiple individual flags at once.

Enabling strict mode is strongly recommended for new projects. It catches null reference errors, implicit any types, and other issues that would otherwise become runtime bugs. The initial investment in type annotations pays off in reduced debugging time.

For existing JavaScript projects migrating to TypeScript, strict mode can be overwhelming. Consider enabling strict flags incrementally—start with noImplicitAny, then add strictNullChecks, and work toward full strict mode.

Individual strict flags can be disabled even with strict: true. This allows you to adopt strict mode while temporarily opting out of specific checks that would require extensive refactoring.

  • strict: Enables all strict type checking options
  • noImplicitAny: Error when type would be inferred as any
  • strictNullChecks: null and undefined are distinct types
  • strictFunctionTypes: Stricter function type checking
  • strictBindCallApply: Check bind, call, apply arguments
  • strictPropertyInitialization: Class properties must be initialized
  • noImplicitThis: Error when this has implicit any type
  • alwaysStrict: Parse in strict mode, emit "use strict"

Module resolution strategies

Module resolution determines how TypeScript finds the code behind import statements. The right strategy depends on your runtime environment and bundler.

For Node.js projects, use moduleResolution: "NodeNext" with module: "NodeNext". This matches Node.js native resolution including package.json exports fields and ESM/CommonJS interop.

For bundler-based projects (Vite, webpack, esbuild), use moduleResolution: "Bundler". This assumes a bundler will handle resolution and enables features like extensionless imports that bundlers support.

The classic resolution strategy is legacy and should not be used for new projects. It does not understand node_modules or package.json and exists only for backward compatibility.

  • NodeNext: For Node.js with native ESM support
  • Node: Legacy Node.js resolution (CommonJS focus)
  • Bundler: For projects using Vite, webpack, or similar
  • Classic: Legacy TypeScript resolution (avoid)
  • Match module and moduleResolution settings

Path aliases and baseUrl

Path aliases eliminate long relative imports like ../../../components/Button. Instead, you write @/components/Button or similar short paths. This improves code readability and makes refactoring easier.

The paths option defines aliases as a mapping from alias patterns to actual paths. Each alias can map to multiple locations, which TypeScript tries in order.

The baseUrl option sets the base directory for non-relative imports. When set to ".", imports like components/Button resolve from the project root. However, baseUrl alone can conflict with node_modules resolution.

Important: TypeScript only resolves path aliases during type checking. Your bundler or runtime must also be configured to understand these aliases. Tools like tsconfig-paths (for Node.js) or bundler plugins handle runtime resolution.

  • paths: Define import aliases like @/* or ~/components/*
  • baseUrl: Base directory for non-relative imports
  • Bundler must also be configured for path aliases
  • Use tsconfig-paths for Node.js runtime resolution
  • Keep aliases consistent across tsconfig and bundler config

Project references for monorepos

Project references split large codebases into smaller, independently compiled pieces. They enable faster incremental builds, better IDE performance, and clearer dependency boundaries.

Each referenced project has its own tsconfig.json with composite: true. The main project references them using the references array. TypeScript builds dependencies before dependents and reuses compiled outputs.

The composite flag tells TypeScript this project can be referenced by others. It requires declaration: true and enforces that all files are included explicitly. This enables TypeScript to determine project outputs without full recompilation.

Use tsc --build (or tsc -b) to compile project references. This builds projects in dependency order and skips up-to-date projects. For large monorepos, this dramatically reduces build times.

  • references: Array of project paths to depend on
  • composite: Mark project as referenceable
  • declaration: Required for composite projects
  • tsc --build: Build mode for project references
  • tsc --build --watch: Watch mode with project references

Performance optimization options

TypeScript offers several options to improve compilation and type checking performance. These become important in large codebases where build times affect developer productivity.

Incremental compilation saves type information between builds, enabling faster subsequent compilations. Combined with project references, this can reduce rebuild times from minutes to seconds.

The skipLibCheck option skips type checking of declaration files (.d.ts). While this reduces safety, it significantly speeds up compilation and avoids issues with conflicting type definitions in dependencies.

Use include and exclude patterns to limit the files TypeScript processes. Excluding test files from the main configuration (with a separate config for tests) reduces the scope of type checking.

  • incremental: Enable incremental compilation
  • tsBuildInfoFile: Location for incremental build info
  • skipLibCheck: Skip type checking .d.ts files
  • Narrow include patterns to essential files
  • Use project references for large codebases
  • Consider separate configs for src and tests

Configuration for different environments

Different environments need different configurations. A library needs declaration files; an application might not. A Node.js backend uses different modules than a React frontend.

For Node.js applications, target recent ES versions (ES2020+), use NodeNext for modules and resolution, and consider CommonJS if your dependencies require it. Enable esModuleInterop for easier imports from CommonJS packages.

For browser applications with bundlers, target ES2020 or later (bundlers handle transpilation), use ESNext modules, and set moduleResolution to Bundler. Include DOM in lib for browser APIs.

For libraries, enable declaration to generate type definitions, consider declarationMap for source navigation, and be conservative with target to support older consumers. Test your types with a separate test configuration.

  • Node.js: NodeNext modules, ES2020+ target, esModuleInterop
  • Browser/Bundler: ESNext modules, Bundler resolution, include DOM
  • Libraries: declaration, declarationMap, conservative target
  • React: Add jsx option (react-jsx for React 17+)

Common mistakes and how to fix them

Certain tsconfig.json mistakes appear repeatedly in projects. Recognizing and fixing them saves hours of debugging.

Forgetting to set moduleResolution causes import failures. When using NodeNext or ESNext modules, always set a matching moduleResolution. The module option alone is not enough.

Conflicting include and exclude patterns lead to missing files. Remember that exclude does not affect files directly imported from included files. Use explicit patterns and test with tsc --listFiles.

Ignoring strict mode catches creates false confidence. TypeScript with strict: false misses many bugs that would fail at runtime. Enable strict mode and address the type errors rather than hiding them.

Mismatched path aliases between tsconfig and bundler cause "module not found" errors at runtime. Always configure aliases in both places and verify with actual imports.

  • Missing moduleResolution with ESM modules
  • Conflicting include/exclude patterns
  • Disabled strict mode hiding real type errors
  • Path aliases only in tsconfig, not bundler
  • Using deprecated options like outFile
  • Forgetting to extend from recommended base configs

Recommended templates by project type

Starting with a recommended base configuration saves time and provides sensible defaults. The @tsconfig collection offers bases for different project types.

For React applications with Vite or similar bundlers, extend @tsconfig/vite-react. This configures JSX, bundler module resolution, and appropriate strict settings.

For Node.js applications, extend @tsconfig/node20 (or your Node version). This sets the correct target, module format, and library types for the Node.js runtime.

For libraries intended for npm publication, start with a strict base and add declaration: true. Consider separate configs for ESM and CommonJS builds if you need to support both.

  • Install: npm install -D @tsconfig/recommended
  • Extend with: "extends": "@tsconfig/recommended"
  • Override specific options as needed
  • @tsconfig/node20 for Node.js applications
  • @tsconfig/vite-react for React with Vite
  • @tsconfig/strictest for maximum type safety

Validating your configuration

After modifying tsconfig.json, validate that your changes work correctly. TypeScript provides tools to inspect the effective configuration.

Run tsc --showConfig to display the fully resolved configuration including inherited options. This reveals the actual values after extending base configs and resolving paths.

Use tsc --listFiles to see exactly which files TypeScript will compile. This catches include/exclude pattern mistakes before they cause build failures.

The JSON Formatter tool helps maintain readable, valid JSON in your tsconfig.json. Syntax errors in configuration files produce cryptic messages; formatting reveals missing commas or braces.

  • tsc --showConfig: Display resolved configuration
  • tsc --listFiles: Show files that will be compiled
  • tsc --noEmit: Type check without writing files
  • Use JSON Formatter to validate and format tsconfig.json
  • Test configuration changes with actual builds

FAQ

Should I use strict mode?

Always. I've never regretted enabling strict mode, only regretted not enabling it sooner. Yes, you'll write more type annotations. But those annotations catch real bugs. Start with noImplicitAny if full strict feels overwhelming.

What is the difference between module and moduleResolution?

module controls output format (CommonJS vs ESM). moduleResolution controls how TypeScript finds imports. They're related but different. My rule: NodeNext for both in Node.js projects, Bundler resolution for frontend apps.

How do I set up @/ path aliases?

Add "paths": {"@/*": ["./src/*"]} to tsconfig. But here is the catch: you need the same config in your bundler too. TypeScript only uses these at compile time, not runtime. This trips up everyone.

Should I use skipLibCheck?

Yes. It skips type-checking node_modules .d.ts files and speeds up builds significantly. I've never had a real bug slip through because of this. The theoretical safety loss isn't worth the build time cost.

What target should I use?

ES2022 for Node.js 18+. ESNext for bundled apps (let the bundler handle transpilation). I stopped worrying about this once I realized bundlers handle browser compatibility better than TypeScript's transpilation.

Are project references worth it?

Only for large monorepos. They add complexity but can dramatically speed up builds by reusing compiled outputs. For most projects, a single tsconfig is simpler and fast enough.

Martin Šikula

Founder of CodeUtil. Web developer building tools I actually use. When I'm not coding, I experiment with productivity techniques (with mixed success).

Related articles