In the previous tutorial, we learned about advanced TypeScript patterns. Now let’s learn about tsconfig.json — the configuration file that controls how TypeScript compiles your code.

By the end of this tutorial, you will know every important compiler option, understand module resolution, set up path aliases, and configure project references for monorepos.

What is tsconfig.json?

tsconfig.json is a JSON file at the root of your project. It tells the TypeScript compiler:

  • Which files to compile
  • What JavaScript version to output
  • How strict the type checking should be
  • How to resolve module imports

When you run npx tsc, it reads this file automatically.

Generating a tsconfig.json

npx tsc --init

This creates a tsconfig.json with sensible defaults. In TypeScript 5.9+, the generated file is smaller and more opinionated — it includes strict: true, module: "nodenext", target: "esnext", verbatimModuleSyntax: true, and isolatedModules: true by default.

The Most Important Options

strict

The single most important option. Turn it on:

{
  "compilerOptions": {
    "strict": true
  }
}

strict: true enables all strict checks at once:

Sub-optionWhat It Does
strictNullChecksnull and undefined are separate types
noImplicitAnyCannot have implicit any types
strictFunctionTypesStricter function parameter checking
strictBindCallApplyType-check bind, call, apply
strictPropertyInitializationClass properties must be initialized
useUnknownInCatchVariablescatch parameter is unknown, not any
noImplicitThisthis must have a type
alwaysStrictEmit "use strict" in every file

Always use strict: true in new projects. It catches many bugs that would otherwise reach production.

target

What version of JavaScript to output:

{
  "compilerOptions": {
    "target": "ES2022"
  }
}
TargetFeatures Included
ES5IE11 compatible (no arrow functions, no let/const)
ES2015Arrow functions, let/const, classes, Promises
ES2020Optional chaining (?.), nullish coalescing (??)
ES2022Top-level await, .at(), error cause
ESNextLatest features (can change between TS versions)

For Node.js 20+, use ES2022. For browsers, check your target browser support.

lib

Specifies which built-in type definitions to include:

{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022", "DOM", "DOM.Iterable"]
  }
}
  • DOM — browser APIs (document, window, fetch)
  • ES2022 — JavaScript built-in types for ES2022
  • DOM.Iterable — iterable DOM collections

For Node.js projects (no browser), remove DOM:

{
  "compilerOptions": {
    "lib": ["ES2022"]
  }
}

module and moduleResolution

These control how TypeScript handles import and export:

{
  "compilerOptions": {
    "module": "Node16",
    "moduleResolution": "Node16"
  }
}
SettingWhen to Use
"Node16" / "NodeNext"Node.js projects (ESM or CommonJS)
"Bundler"Projects using Vite, webpack, esbuild, Next.js
"ESNext"Libraries targeting modern runtimes

For most projects in 2026:

  • Node.js backend: "NodeNext" for both (use "Node16" if targeting Node 16 specifically)
  • Frontend with bundler: "Bundler" for moduleResolution
  • Next.js: Uses its own defaults (usually "Bundler")

outDir and rootDir

{
  "compilerOptions": {
    "rootDir": "./src",
    "outDir": "./dist"
  }
}
  • rootDir — where your source files are
  • outDir — where compiled JavaScript goes

The folder structure inside rootDir is preserved in outDir.

include, exclude, and files

Control which files are compiled:

{
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}
  • include — glob patterns for files to compile
  • exclude — glob patterns to skip
  • files — explicit list of files (rarely used)

Path Aliases

Long relative imports are hard to read:

import { User } from "../../../models/user";

Path aliases make them clean:

import { User } from "@/models/user";

Setting Up Path Aliases

{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"],
      "@models/*": ["./src/models/*"],
      "@utils/*": ["./src/utils/*"]
    }
  }
}

Note: Since TypeScript 4.1, baseUrl is no longer required when using paths. If you do set baseUrl, your path values must be relative to it. Keeping baseUrl: "." still works but is optional.

Important: paths only tells TypeScript how to resolve types. Your build tool (Vite, webpack, Next.js) also needs to understand these aliases.

Vite Configuration

// vite.config.ts
import { defineConfig } from "vite";
import path from "path";
import { fileURLToPath } from "url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));

export default defineConfig({
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
});

Next.js

Next.js reads paths from tsconfig.json automatically. No extra configuration needed.

Declaration Files

For publishing libraries:

{
  "compilerOptions": {
    "declaration": true,
    "declarationMap": true,
    "emitDeclarationOnly": false
  }
}
  • declaration — generate .d.ts files
  • declarationMap — generate source maps for declarations (lets users “Go to Definition” to your TypeScript source)
  • emitDeclarationOnly — only generate .d.ts, no .js (when using a separate bundler)

Project References

Project references let you split a monorepo into separate TypeScript projects that reference each other:

my-monorepo/
  packages/
    shared/
      tsconfig.json
      src/
        types.ts
    server/
      tsconfig.json
      src/
        index.ts
    client/
      tsconfig.json
      src/
        App.tsx
  tsconfig.json

Root tsconfig.json

{
  "references": [
    { "path": "./packages/shared" },
    { "path": "./packages/server" },
    { "path": "./packages/client" }
  ],
  "files": []
}

Package tsconfig.json

{
  "compilerOptions": {
    "composite": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true
  },
  "references": [
    { "path": "../shared" }
  ],
  "include": ["src/**/*"]
}

Key options:

  • composite: true — required for referenced projects
  • references — list of projects this project depends on

Build everything:

npx tsc --build

This builds projects in the correct order based on dependencies.

isolatedModules

{
  "compilerOptions": {
    "isolatedModules": true
  }
}

This ensures each file can be compiled independently. It is recommended when using Babel, SWC, or esbuild because they compile one file at a time and cannot see other files. Vite and Next.js also benefit from it, though they do not strictly require it.

With isolatedModules, TypeScript will error on:

  • const enum (needs whole-program compilation)
  • Re-exporting types without type keyword
  • Namespace declarations that contain runtime values in non-module files

erasableSyntaxOnly (TypeScript 5.8+)

{
  "compilerOptions": {
    "erasableSyntaxOnly": true
  }
}

This option was added for Node.js --strip-types mode. It ensures you only use TypeScript syntax that can be removed by simply erasing type annotations, without any code transformations.

It disallows:

  • enum declarations (they generate JavaScript code)
  • namespace with runtime values
  • Parameter properties in constructors (constructor(public name: string))
  • import = and export = syntax

verbatimModuleSyntax

{
  "compilerOptions": {
    "verbatimModuleSyntax": true
  }
}

This replaces the older importsNotUsedAsValues option. It requires you to use import type for type-only imports:

// With verbatimModuleSyntax: true
import type { User } from "./user";     // OK — type-only
import { createUser } from "./user";    // OK — value import

// import { User } from "./user";       // Error — must use 'import type'

This makes it clear which imports are types (removed at runtime) and which are values (kept at runtime).

Node.js Backend

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "declaration": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true
  },
  "include": ["src/**/*"]
}

React + Vite

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

Using Community Presets

Instead of writing your own config, use community presets:

npm install --save-dev @tsconfig/node20
{
  "extends": "@tsconfig/node20/tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"]
}

Available presets: @tsconfig/node20, @tsconfig/strictest, @tsconfig/vite-react.

What’s Next?

In the next tutorial, we will build a complete CLI tool with TypeScript — a bookmark manager that uses commander, Zod, and chalk.