Skip to main content

tsconfig & Tooling

The tsconfig.json file is the control centre of a TypeScript project. It tells the compiler what files to process, how strict to be, what JavaScript version to emit, and dozens of other options. Getting it right is the difference between a smooth developer experience and a frustrating one.

tsconfig.json structure

A tsconfig.json has three main sections:

{
"compilerOptions": { }, // How to compile
"include": [], // Which files to include
"exclude": [], // Which files to exclude
"extends": "" // Inherit from another tsconfig
}

Generate a commented reference file with all options:

npx tsc --init

The most important compiler options

strict

{ "strict": true }

strict: true is a shorthand that enables a bundle of checks. Always enable this in new projects.

It turns on:

FlagWhat it catches
strictNullChecksnull and undefined are not assignable to other types
strictFunctionTypesFunction parameter types are checked contravariantly
strictBindCallApplybind, call, and apply are type-checked properly
strictPropertyInitializationClass properties must be initialized in the constructor
noImplicitAnyVariables without a type annotation cannot be implicitly any
noImplicitThisthis must have an explicit type
alwaysStrictEmits "use strict" in every file
useUnknownInCatchVariablesCatch clause variables are unknown instead of any (TS 4.4+)

You can enable individual strict flags separately if you need to adopt them gradually:

{
"compilerOptions": {
"strict": false,
"strictNullChecks": true,
"noImplicitAny": true
}
}

target

{ "target": "ES2022" }

target controls which JavaScript version tsc emits. TypeScript down-compiles modern syntax for older environments:

TargetUse when
ES5Supporting very old browsers (IE11) -- rare in 2026
ES2017Broad browser support, includes async/await natively
ES2020Modern browsers and Node.js 14+, includes optional chaining, nullish coalescing
ES2022Node.js 18+, modern browsers -- recommended default
ESNextAlways the latest features -- good for bundler projects

If your bundler (Vite, esbuild, webpack) handles downcompilation, set target: "ESNext" and let the bundler control the output.

module and moduleResolution

{
"module": "NodeNext",
"moduleResolution": "NodeNext"
}

These options control the module system:

ScenariomodulemoduleResolution
Node.js with ESM ("type": "module")NodeNextNodeNext
Node.js with CJSCommonJSNode
Bundler (Vite, webpack, Rollup)ESNextBundler
DenoESNextBundler

With moduleResolution: "NodeNext", TypeScript requires explicit file extensions in imports:

// Required with NodeNext
import { helper } from "./helper.js"; // .js extension (TypeScript resolves to .ts)

With moduleResolution: "Bundler", extensions are optional (the bundler resolves them).

outDir and rootDir

{
"rootDir": "./src",
"outDir": "./dist"
}

rootDir tells TypeScript where your source files are. outDir is where compiled .js files (and .d.ts files) are written. The directory structure under rootDir is mirrored in outDir:

src/
index.ts
utils/
format.ts

dist/
index.js
utils/
format.js

lib

{ "lib": ["ES2022", "DOM"] }

lib controls which built-in type definitions are available. TypeScript uses these to know what APIs exist:

lib valueIncludes
ES2022All ES2022 built-ins (Array, Map, Promise, etc.)
DOMBrowser APIs (window, document, fetch, etc.)
DOM.IterableIterable DOM collections
WebWorkerWeb Worker APIs

For Node.js projects without browser code, omit DOM:

{ "lib": ["ES2022"] }

For browser projects:

{ "lib": ["ES2022", "DOM", "DOM.Iterable"] }

baseUrl and paths

{
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@utils/*": ["src/utils/*"],
"@types/*": ["src/types/*"]
}
}

Path aliases reduce relative import hell. The baseUrl is the base directory for resolving non-relative module names.

Other useful strict options

{
"compilerOptions": {
"noUnusedLocals": true, // Error on unused local variables
"noUnusedParameters": true, // Error on unused function parameters
"noImplicitReturns": true, // All code paths in a function must return
"noFallthroughCasesInSwitch": true, // No accidental switch fallthrough
"exactOptionalPropertyTypes": true, // Distinguish missing vs undefined properties
"noPropertyAccessFromIndexSignature": true // Force bracket notation for index signatures
}
}

skipLibCheck and esModuleInterop

{
"skipLibCheck": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
}
OptionWhat it does
skipLibCheckSkip type-checking .d.ts files -- much faster builds, avoids conflicts
esModuleInteropEnables import React from 'react' (default import) instead of import * as React
forceConsistentCasingInFileNamesPrevents case-sensitivity bugs on case-insensitive file systems (macOS, Windows)
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowImportingTsExtensions": true,
"noEmit": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

When using a bundler, set noEmit: true -- the bundler compiles the code, and TypeScript is used only for type checking.

extends -- sharing tsconfig

Large monorepos often have a base tsconfig that projects extend:

// tsconfig.base.json
{
"compilerOptions": {
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
}
}
// packages/api/tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"rootDir": "src"
}
}

The community also maintains published tsconfig presets:

npm install --save-dev @tsconfig/node22
npm install --save-dev @tsconfig/strictest
{ "extends": "@tsconfig/node22/tsconfig.json" }

Project references

For large codebases with multiple packages (monorepos), project references let you split a TypeScript project into smaller parts that build incrementally:

// tsconfig.json (root)
{
"files": [],
"references": [
{ "path": "./packages/core" },
{ "path": "./packages/api" },
{ "path": "./packages/web" }
]
}
// packages/core/tsconfig.json
{
"compilerOptions": {
"composite": true, // Required for project references
"declaration": true, // Required: produces .d.ts for dependents
"outDir": "dist",
"rootDir": "src"
}
}
// packages/api/tsconfig.json
{
"compilerOptions": {
"composite": true,
"outDir": "dist",
"rootDir": "src"
},
"references": [
{ "path": "../core" } // api depends on core
]
}

Build with --build flag (incremental compilation):

npx tsc --build # Build all referenced projects
npx tsc --build --watch # Watch mode for all projects
npx tsc --build --clean # Remove all build output

ts-node and tsx

For development, you often want to run TypeScript files directly without a separate compile step.

ts-node

npm install --save-dev ts-node
npx ts-node src/index.ts # Run directly
npx ts-node --esm src/index.ts # ESM mode

Configure ts-node in tsconfig.json or a separate tsconfig.node.json:

// tsconfig.json
{
"ts-node": {
"esm": true,
"experimentalSpecifierResolution": "node"
}
}

tsx (faster alternative)

tsx uses esbuild to transpile TypeScript at near-native speed -- much faster than ts-node:

npm install --save-dev tsx
npx tsx src/index.ts # Run directly
npx tsx watch src/index.ts # Watch mode (like nodemon + ts-node)

Note: tsx transpiles (strips types) but does not type-check. Run tsc --noEmit separately in CI to verify types while using tsx for fast local execution.

package.json scripts

{
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit",
"dev": "tsx watch src/index.ts",
"start": "node dist/index.js",
"clean": "rm -rf dist"
}
}

TypeScript with ESLint

ESLint with TypeScript-aware rules catches problems that the compiler does not:

npm install --save-dev \
eslint \
@eslint/js \
typescript-eslint

ESLint flat config (eslint.config.js)

// eslint.config.js
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";

export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
{
languageOptions: {
parserOptions: {
project: true,
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
// Enforce consistent type imports
"@typescript-eslint/consistent-type-imports": [
"error",
{ prefer: "type-imports" },
],
// Warn on floating promises (missing await)
"@typescript-eslint/no-floating-promises": "error",
// Disallow explicit any
"@typescript-eslint/no-explicit-any": "warn",
// Require explicit return types on public functions
"@typescript-eslint/explicit-function-return-type": [
"warn",
{ allowExpressions: true },
],
// Require nullish coalescing instead of ||
"@typescript-eslint/prefer-nullish-coalescing": "error",
// Prefer optional chaining
"@typescript-eslint/prefer-optional-chain": "error",
},
},
{
// Relax rules for test files
files: ["**/*.test.ts", "**/*.spec.ts"],
rules: {
"@typescript-eslint/no-explicit-any": "off",
},
},
);

Useful ESLint TypeScript rules

RuleWhat it catches
@typescript-eslint/no-explicit-anyBans any -- forces better types
@typescript-eslint/no-floating-promisesPromises not awaited or handled
@typescript-eslint/no-misused-promisesPromises used where void is expected
@typescript-eslint/await-thenableAwaiting non-promise values
@typescript-eslint/prefer-nullish-coalescing`
@typescript-eslint/prefer-optional-chaina && a.b vs a?.b
@typescript-eslint/consistent-type-importsEnforces import type for type-only imports
@typescript-eslint/no-unnecessary-type-assertionCatches redundant as assertions

Useful tsc CLI flags

FlagPurpose
--noEmitType-check only, do not write any files
--watch / -wRecompile on file changes
--build / -bUse project references build mode
--strictEnable all strict checks (overrides tsconfig)
--listFilesPrint all files included in the compilation
--diagnosticsShow compilation statistics (useful for debugging slow builds)
--generateTraceGenerate a trace file for performance analysis
--skipLibCheckSkip type checking of declaration files
--prettyFormat output with colour and formatting (default: true)

Summary

  • strict: true is the most important tsconfig option -- enable it in every new project
  • target controls the JavaScript version emitted; module/moduleResolution control the module system
  • lib controls which built-in type definitions are available
  • baseUrl and paths enable clean path aliases -- configure both tsconfig and your bundler
  • extends lets you share a base tsconfig across a monorepo or use community presets like @tsconfig/node22
  • Project references with composite: true enable incremental, distributed compilation for large codebases
  • ts-node runs TypeScript directly; tsx is faster but skips type checking
  • ESLint with typescript-eslint catches runtime problems that the type checker cannot: floating promises, misused nullability, redundant casts

Next up: TypeScript in Practice -- TypeScript with Node.js, TypeScript with React, common real-world patterns, and migrating an existing JavaScript project to TypeScript.