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-option | What It Does |
|---|---|
strictNullChecks | null and undefined are separate types |
noImplicitAny | Cannot have implicit any types |
strictFunctionTypes | Stricter function parameter checking |
strictBindCallApply | Type-check bind, call, apply |
strictPropertyInitialization | Class properties must be initialized |
useUnknownInCatchVariables | catch parameter is unknown, not any |
noImplicitThis | this must have a type |
alwaysStrict | Emit "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"
}
}
| Target | Features Included |
|---|---|
ES5 | IE11 compatible (no arrow functions, no let/const) |
ES2015 | Arrow functions, let/const, classes, Promises |
ES2020 | Optional chaining (?.), nullish coalescing (??) |
ES2022 | Top-level await, .at(), error cause |
ESNext | Latest 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 ES2022DOM.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"
}
}
| Setting | When 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"formoduleResolution - Next.js: Uses its own defaults (usually
"Bundler")
outDir and rootDir
{
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
}
}
rootDir— where your source files areoutDir— 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 compileexclude— glob patterns to skipfiles— 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,
baseUrlis no longer required when usingpaths. If you do setbaseUrl, your path values must be relative to it. KeepingbaseUrl: "."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.tsfilesdeclarationMap— 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 projectsreferences— 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
typekeyword - 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:
enumdeclarations (they generate JavaScript code)namespacewith runtime values- Parameter properties in constructors (
constructor(public name: string)) import =andexport =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).
Recommended Configurations
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.