cheat sheet

tsconfig.json Reference

Complete reference for tsconfig.json — compiler options for type checking, module resolution, output, paths, and JSX. Includes ready-to-use presets for Node 20, browser libraries, and Vite/React apps.

tsconfig.json Reference

What it is

tsconfig.json is the TypeScript project configuration file placed at the root of a TypeScript project. It controls which files are included in the compilation and how the compiler processes them. Options are split across two concerns: which files to compile (include, exclude, files, references) and how to compile them (compilerOptions).

Basic structure

json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "strict": true,
    "outDir": "dist"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"],
  "references": []
}

Top-level fields

FieldPurpose
compilerOptionsCompiler behavior settings
includeGlob patterns for files to include
excludeGlob patterns to exclude (defaults: node_modules, outDir)
filesExplicit list of files (overrides include)
extendsPath to a base tsconfig to inherit from
referencesSub-project references for tsc --build

Type checking options

json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "useUnknownInCatchVariables": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noPropertyAccessFromIndexSignature": true
  }
}

Key options explained

strict — Enables a bundle of strict checks. Equivalent to setting all of the following to true: noImplicitAny, strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, useUnknownInCatchVariables. Always enable this.

noImplicitAny — Errors when TypeScript infers type any due to missing annotations. Forces explicit typing.

strictNullChecks — Makes null and undefined distinct types. Without this, every type is implicitly nullable — a common source of runtime bugs.

strictFunctionTypes — Enforces stricter checking of function parameter types (contravariance). Catches subtle callback type errors.

noUncheckedIndexedAccess — Array index access returns T | undefined instead of T. Prevents silent out-of-bounds bugs.

typescript
// noUncheckedIndexedAccess: true
const arr = [1, 2, 3];
const first = arr[0]; // type: number | undefined
if (first !== undefined) {
  console.log(first.toFixed(2)); // safe
}

exactOptionalPropertyTypes — Distinguishes between a property being absent and being explicitly set to undefined.

typescript
// exactOptionalPropertyTypes: true
interface Config {
  timeout?: number;
}
const c: Config = { timeout: undefined }; // Error — must omit the key instead

Module and output options

json
{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "target": "ES2022",
    "outDir": "dist",
    "rootDir": "src",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "removeComments": false
  }
}

module values

ValueUse case
CommonJSNode.js with require()
ESNextBundlers (Vite, esbuild, Webpack)
NodeNextNode.js with native ESM (.mjs / "type": "module")
Node16Same as NodeNext but pinned to Node 16 semantics
PreserveKeep the module format as authored (TS 5.4+)

moduleResolution values

ValueMatches
nodeClassic Node.js require() resolution (legacy)
bundlerVite, esbuild, Webpack — allows extensionless imports
node16 / nodenextNative Node.js ESM — requires explicit .js extensions in imports

module: "NodeNext" requires import paths in your source to use .js extensions even though the source files are .ts. This is because Node.js resolves the compiled output, not the source.

target values

Sets the JavaScript version of the emitted output. TypeScript will down-compile any syntax newer than the target.

ValueMinimum runtime
ES2020Node 14+, modern browsers
ES2022Node 16+, modern browsers
ESNextLatest supported features (tracks TS release)

Declaration and map options

OptionEffect
declarationEmit .d.ts type declaration files
declarationMapEmit .d.ts.map files (go-to-definition jumps to source)
sourceMapEmit .js.map for runtime debugging
inlineSourceMapEmbed source map inside .js instead of separate file

Path and import options

json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@app/*": ["src/app/*"],
      "@lib/*": ["src/lib/*"],
      "@/*": ["src/*"]
    },
    "rootDirs": ["src", "generated"]
  }
}

baseUrl — The base directory for non-relative module names. Usually set to . (project root) or src.

paths — Map import aliases to file paths. Must be used alongside a bundler or a path resolver plugin (tsconfig-paths) at runtime since tsc does not rewrite import paths.

rootDirs — Tells TypeScript to treat multiple directories as a single virtual root. Useful when generated files live in a separate directory.

JSX

json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "react"
  }
}
ValueDescription
reactClassic React.createElement transform (React 16 and older)
react-jsxNew JSX transform (React 17+, no import required)
react-jsxdevSame as react-jsx with dev runtime (extra warnings)
preserveKeep JSX as-is for a bundler to handle (Vite, esbuild)

For Solid.js: "jsxImportSource": "solid-js/h". For Preact: "jsxImportSource": "preact".

Extending a base config

The extends field inherits all options from another tsconfig and allows overriding individual fields.

json
{
  "extends": "@tsconfig/node20/tsconfig.json",
  "compilerOptions": {
    "outDir": "dist"
  },
  "include": ["src"]
}

Install community base configs from npm:

bash
npm install -D @tsconfig/node20
npm install -D @tsconfig/strictest
npm install -D @tsconfig/vite-react

Output: (none — exits 0 on success)

Common presets

Node 20 application

json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": ["ES2022"],
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitReturns": true,
    "outDir": "dist",
    "rootDir": "src",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

Browser library (published to npm)

json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "dist",
    "rootDir": "src",
    "skipLibCheck": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}

Vite + React application

json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "jsx": "react-jsx",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    },
    "noEmit": true,
    "skipLibCheck": true
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

tsconfig.node.json (for Vite config file itself):

json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "composite": true
  },
  "include": ["vite.config.ts"]
}

For Vite projects, set noEmit: true in the main tsconfig — Vite handles the actual compilation. Use tsc --noEmit in CI only for type-checking.

Complete option quick-reference

json
{
  "compilerOptions": {
    // Type checking
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,

    // Module
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "esModuleInterop": true,

    // Output
    "target": "ES2022",
    "outDir": "dist",
    "rootDir": "src",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,

    // Paths
    "baseUrl": ".",
    "paths": { "@/*": ["src/*"] },

    // Misc
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

How tsc reads tsconfig.json

When you run tsc with no arguments, the compiler walks up from the current working directory looking for a tsconfig.json. Once found, it parses the JSON, resolves extends chains, expands include/exclude/files globs, and finally invokes the compilation. Knowing the exact order helps debug missing-file errors and surprise emit shapes.

bash
npx tsc --showConfig

Output:

text
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "skipLibCheck": true
  },
  "files": [],
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "node_modules",
    "dist"
  ]
}

--showConfig is the canonical way to inspect what tsc actually sees — after extends resolution and after every glob is expanded. If your build emits the wrong shape, this is the first command to run.

To run tsc against a different config without changing directories:

bash
npx tsc -p tsconfig.build.json

Output: (none — exits 0 on success)

The -p (project) flag accepts a directory (looking for tsconfig.json inside) or a direct path to any .json file.

Type checking deep dive

Each of the strict-family flags addresses a specific category of subtle bugs. Understanding what each one catches makes it easier to migrate a legacy codebase one flag at a time.

strict: the bundle

strict: true is shorthand for enabling seven sub-flags simultaneously. The list grows occasionally — TypeScript may add a new strict check in a future release. Enabling strict opts in to whatever the current TypeScript version considers strict.

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

Equivalent to setting all of these explicitly:

json
{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "useUnknownInCatchVariables": true,
    "alwaysStrict": true
  }
}

For migrating an existing JS codebase, enable strict: true then disable individual flags as needed: "strict": true, "noImplicitAny": false. This is the recommended approach over enabling flags one by one — it lets future TS versions add new strict checks that your team has opted into.

useUnknownInCatchVariables

Before TS 4.4, catch (e) typed e as any. With useUnknownInCatchVariables: true (part of strict), e is unknown, forcing a runtime narrow before use.

typescript
try {
  doSomething();
} catch (e) {
  if (e instanceof Error) {
    console.error(e.message);
  } else {
    console.error("Unknown error:", e);
  }
}

Without the flag, e.message would compile without checks and crash if e is a number or null. The narrow is required for type safety.

strictPropertyInitialization

Class fields must be initialised in the constructor or have an initialiser:

typescript
class User {
  // Error — strictPropertyInitialization
  name: string;

  // OK — definite assignment assertion
  email!: string;

  // OK — initialiser
  role: string = "member";

  // OK — initialised in constructor
  id: number;
  constructor() {
    this.id = Math.random();
  }
}

The ! (definite assignment assertion) tells TS "I promise this is initialised before any read". Use sparingly — it's an escape hatch.

exactOptionalPropertyTypes

Without this flag, an optional property name?: string is treated as string | undefined. With it, the property must either be absent or hold a string — explicitly passing undefined errors.

typescript
interface Config { timeout?: number; }

// Default (exactOptionalPropertyTypes: false)
const a: Config = { timeout: undefined }; // OK

// Strict (exactOptionalPropertyTypes: true)
const b: Config = { timeout: undefined };
// Error TS2375: Type '{ timeout: undefined; }' is not assignable to type 'Config'
//   with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the type
//   of the target.
const c: Config = {};                     // OK — absent
const d: Config = { timeout: 1000 };      // OK — present with value

This flag catches a common bug: { ...defaults, timeout: undefined } silently overrides the default. Most teams enable it; some find it too noisy on existing codebases.

noUncheckedIndexedAccess

Index access (arr[0], obj["key"]) returns T | undefined instead of T. Prevents silent out-of-bounds bugs.

typescript
// noUncheckedIndexedAccess: false (default)
const arr = [1, 2, 3];
const first = arr[100]; // type: number, value: undefined — silent bug

// noUncheckedIndexedAccess: true
const arr2 = [1, 2, 3];
const first2 = arr2[100]; // type: number | undefined — must narrow
if (first2 !== undefined) {
  console.log(first2.toFixed(2));
}

Pair this with Object.entries() and Object.keys() for safer iteration:

typescript
const record: Record<string, number> = { a: 1, b: 2 };
const val = record["c"]; // type: number | undefined

noImplicitOverride

Forces explicit override keyword on method overrides in subclasses. Catches typo-renamed methods that silently no longer override anything.

typescript
class Base {
  greet() { return "hi"; }
}

class Child extends Base {
  // Error — missing 'override' keyword
  greet() { return "hello"; }

  // OK
  override greet() { return "hello"; }
}

If you rename Base.greet to Base.greetUser, every Child.greet becomes a new method instead of an override — noImplicitOverride catches this immediately.

Other useful checks

FlagCatches
noImplicitReturnsA function with mixed return / no return paths
noFallthroughCasesInSwitchMissing break in switch cases
noUnusedLocalsDeclared-but-unused local variables
noUnusedParametersDeclared-but-unused function parameters (prefix with _ to allow)
allowUnreachableCode: falseCode after return / throw
allowUnusedLabels: falseLabels not referenced by any break/continue
noPropertyAccessFromIndexSignatureobj.dynamic when only obj["dynamic"] should be allowed
json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitOverride": true,
    "noPropertyAccessFromIndexSignature": true
  }
}

Source maps and declaration files

The emit options control what tsc writes to disk alongside the compiled JavaScript. Each artifact has a distinct purpose.

Source maps

A source map is a .js.map file that maps positions in the compiled JavaScript back to positions in the original TypeScript. Browsers, Node debuggers, and stack-trace tools read it to show the original .ts in error messages.

json
{
  "compilerOptions": {
    "sourceMap": true
  }
}
bash
tsc
ls dist/

Output:

text
index.js
index.js.map
util.js
util.js.map
OptionEffect
sourceMap: trueEmit external .js.map file
inlineSourceMap: trueEmbed source map at the bottom of .js as a data URL
inlineSources: trueInclude the original .ts content in the source map itself
sourceRoot: "/src"Prefix in the map's sources array (useful when serving from a CDN)
mapRoot: "/maps"Tell consumers where to fetch the .map files from

In Node, run with --enable-source-maps to get TS line numbers in stack traces:

bash
node --enable-source-maps dist/index.js

Output:

text
Error: Boom
    at greet (src/index.ts:7:9)
    at main (src/index.ts:12:3)

Without the flag, the stack would point at dist/index.js.

Declaration files

declaration: true emits a .d.ts for every .ts source file. These are the "headers" that downstream consumers see when they import your library.

json
{
  "compilerOptions": {
    "declaration": true,
    "declarationMap": true,
    "emitDeclarationOnly": false
  }
}
OptionEffect
declarationEmit .d.ts files
declarationMapEmit .d.ts.map — lets editors "go to definition" jump to source
emitDeclarationOnlySkip .js emit entirely (for projects that bundle JS separately)
declarationDirOverride the output directory for .d.ts (defaults to outDir)

A library's typical declaration-emit setup:

json
{
  "compilerOptions": {
    "declaration": true,
    "declarationMap": true,
    "outDir": "dist",
    "rootDir": "src"
  }
}
bash
tsc
ls dist/

Output:

text
index.d.ts
index.d.ts.map
index.js
index.js.map
util.d.ts
util.d.ts.map
util.js
util.js.map

emitDeclarationOnly: true is useful when a faster bundler (esbuild, swc, Bun) handles JS emit but you still need TypeScript's .d.ts:

bash
# Emit only .d.ts files
tsc --emitDeclarationOnly

# Build JS with a fast bundler in parallel
esbuild src/index.ts --bundle --outdir=dist --format=esm

Output: (none — exits 0 on success)

JSX deep dive

The jsx option controls how .tsx files are compiled. Each value produces dramatically different output.

json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "react",
    "jsxFactory": "h",
    "jsxFragmentFactory": "Fragment"
  }
}
jsx valueOutput for <div />When to use
reactReact.createElement('div')React 16 and older
react-jsximport { jsx } from 'react/jsx-runtime'; jsx('div')React 17+ (no React import needed in source)
react-jsxdevSame as react-jsx with dev runtimeDevelopment builds with line/column tracking
preserve<div /> (untouched)Vite/esbuild that handles JSX downstream
react-native<div /> (untouched, but .js extension)React Native bundlers

The jsxImportSource option is for non-React JSX runtimes:

json
// Preact
{ "jsx": "react-jsx", "jsxImportSource": "preact" }

// Solid
{ "jsx": "preserve", "jsxImportSource": "solid-js" }

// Emotion (classic JSX)
{ "jsx": "react", "jsxFactory": "jsx", "jsxFragmentFactory": "Fragment" }

For Astro projects, jsx: "preserve" is the default — Astro's own preprocessor handles .astro and JSX in one pass.

Decorators and metadata

Decorators live in their own compilation pass. Two distinct decorator systems exist; the compiler options differ.

json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "useDefineForClassFields": true
  }
}
OptionWhen to use
experimentalDecorators: trueLegacy decorator API — Angular, NestJS, TypeORM, class-validator
(no flag)TC39 standard decorators (TS 5.0+) — modern framework-agnostic code
emitDecoratorMetadata: trueStores parameter types at runtime via Reflect.metadata; requires experimentalDecorators
useDefineForClassFields: trueUse ES2022 class-field semantics (define not Object.assign)

You cannot mix the legacy and standard APIs in the same project. See decorators for the full comparison.

Project references in detail

references and composite: true together turn a single tsconfig into a graph of incrementally-built sub-projects. The compiler can skip whole projects whose inputs are unchanged, dramatically speeding up monorepo builds.

json
// tsconfig.json (root)
{
  "files": [],
  "references": [
    { "path": "./packages/shared" },
    { "path": "./packages/ui" },
    { "path": "./packages/api" },
    { "path": "./apps/web" }
  ]
}

Each referenced project sets composite: true:

json
// packages/shared/tsconfig.json
{
  "compilerOptions": {
    "composite": true,
    "outDir": "dist",
    "rootDir": "src",
    "declaration": true,
    "declarationMap": true
  },
  "include": ["src/**/*"]
}

Build the whole graph in dependency order:

bash
npx tsc --build

Output:

text
(no output — exit code 0)

See project references for the deep dive on --build, .tsbuildinfo caching, and Turborepo/Nx integration.

The @tsconfig/* ecosystem

Maintaining a custom tsconfig per project is repetitive. The @tsconfig/* packages on npm are community-maintained presets you can extends from.

PackageTargets
@tsconfig/node20Node 20 with NodeNext modules and strict checks
@tsconfig/node22Node 22 with the latest module settings
@tsconfig/strictestEvery strict flag turned on, including noUncheckedIndexedAccess and exactOptionalPropertyTypes
@tsconfig/recommendedSensible defaults; less aggressive than strictest
@tsconfig/vite-reactVite + React app with jsx: "react-jsx" and moduleResolution: "bundler"
@tsconfig/nextNext.js apps
@tsconfig/remixRemix apps
@tsconfig/svelteSvelte projects
@tsconfig/denoDeno scripts
@tsconfig/bunBun-first projects
@tsconfig/cloudflare-workersCloudflare Workers with WebWorker lib and Bundler resolution
@tsconfig/create-react-appCRA-compatible (legacy)

Install and extend:

bash
npm install -D @tsconfig/node20 @tsconfig/strictest

Output:

text
added 2 packages in 0.6s
json
{
  "extends": ["@tsconfig/node20/tsconfig.json", "@tsconfig/strictest/tsconfig.json"],
  "compilerOptions": {
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src"]
}

The right-most extends wins on conflicts (TS 5.0+). Layer presets: a base (@tsconfig/node20) plus a strictness modifier (@tsconfig/strictest).

Verify the merged config:

bash
npx tsc --showConfig | head -20

Output:

text
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": ["ES2022"],
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "outDir": "./dist",
    "rootDir": "./src"
  }
}

paths and bundler agreement

paths aliases (@/components/Buttonsrc/components/Button.ts) only affect TypeScript's type-checking — the emitted JavaScript still contains the literal alias. At runtime, your bundler (or a path-resolver shim) must agree on the same mapping or the import will fail.

json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@lib/*": ["src/lib/*"]
    }
  }
}

The four runtime stories:

  1. Vite — mirror in vite.config.ts:
typescript
import { defineConfig } from "vite";
import { fileURLToPath, URL } from "node:url";

export default defineConfig({
  resolve: {
    alias: {
      "@": fileURLToPath(new URL("./src", import.meta.url)),
      "@components": fileURLToPath(new URL("./src/components", import.meta.url)),
      "@lib": fileURLToPath(new URL("./src/lib", import.meta.url)),
    },
  },
});
  1. esbuild — use the tsconfig-paths plugin:
bash
npm install -D esbuild esbuild-plugin-tsconfig-paths

Output:

text
added 2 packages in 0.4s
  1. Node directly — use tsconfig-paths/register:
bash
node --import tsconfig-paths/register dist/index.js

Output: (none — exits 0 on success)

  1. Bun — Bun reads tsconfig.json paths natively:
bash
bun run src/index.ts

Output:

text
Hello from TypeScript

If paths is set but the bundler doesn't know about it, you get a runtime Cannot find module '@/foo' error — code that type-checks but doesn't run. Always keep the two sides in sync, or use workspaces instead (see project references).

skipLibCheck — the universal escape hatch

skipLibCheck: true is the single most-recommended option for shipping projects. It tells tsc not to type-check .d.ts files in node_modules — only your source.

json
{
  "compilerOptions": {
    "skipLibCheck": true
  }
}

Without it, a single buggy @types/foo package can break your entire build. With it, your code still gets full type-checking, but the upstream .d.ts files are taken at face value.

bash
# Without skipLibCheck — slow and brittle
time npx tsc --noEmit

Output:

text
src/main.ts:1:10 - error TS7016: Could not find a declaration for ...
node_modules/@types/some-pkg/index.d.ts:42:10 - error TS2304: Cannot find ...

Found 3 errors in 2 files.

real    0m4.821s
bash
# With skipLibCheck — fast and tolerant
time npx tsc --noEmit

Output:

text
src/main.ts:1:10 - error TS7016: Could not find a declaration for ...

Found 1 error in 1 file.

real    0m1.412s

The trade-off: if you author a library and ship .d.ts files, skipLibCheck: true means consumers won't see errors in your declarations. Always run a CI step with skipLibCheck: false before publishing.

Lib option — built-in type libraries

The lib option lists which TypeScript built-in libraries to include. It's separate from target because some runtimes (browsers, web workers, Node) expose different globals.

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

Common lib values:

ValueAdds
ES2015ES2024ECMAScript globals for that year (Promise, Map, Array.prototype.flat, etc.)
ESNextLatest spec features (proposals at stage 3+)
DOMwindow, document, HTMLElement, etc.
DOM.IterableNodeList and HTMLCollection iteration support
WebWorkerWorker globals (self, postMessage)
WebWorker.IterableIteration support for WebWorker globals
ScriptHostWindows ScriptHost APIs (legacy)
decoratorsSymbol.metadata (TS 5.2+)
decorators.legacyLegacy decorator metadata typings

For a Node-only project:

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

For a Cloudflare Worker:

json
{
  "compilerOptions": {
    "lib": ["ES2022", "WebWorker"],
    "types": ["@cloudflare/workers-types"]
  }
}

If you omit lib, TypeScript defaults to a sensible set based on target — including DOM. Browser projects don't need to set lib explicitly; Node projects should set lib: ["ES2022"] (no DOM) to avoid mistakenly using browser globals.

Common pitfalls

  1. module: NodeNext without .js extensions — every relative import must end in .js even though sources are .ts. Add them, or switch to moduleResolution: bundler.
  2. paths working in the editor but breaking at runtime — the editor uses paths; Node doesn't. Either set up tsconfig-paths/register or mirror the alias in the bundler.
  3. extends from a package not in devDependencies — CI fails on missing base config. Always declare @tsconfig/* in devDependencies.
  4. composite: true without outDir and rootDir — TS errors because composite projects must emit. Set both.
  5. skipLibCheck: false on a large codebase — every @types/* package's bugs become your errors. Default to true; only flip to false on CI before publishing.
  6. target: ES5 with modern syntaxasync/await compiled to ES5 generates a huge polyfill helper. Pick ES2017 or later.
  7. strict: true plus noImplicitAny: false — TypeScript silently re-enables noImplicitAny because it's part of strict. Either keep strict: true (turning everything on) or set strict: false and pick individual flags.
  8. Setting module without moduleResolution — defaults can surprise: module: NodeNext defaults moduleResolution to NodeNext, but module: ESNext defaults it to Classic (effectively broken). Always set both.
  9. include and files togetherfiles takes precedence and is exact-match. If you set files: ["src/main.ts"], only that one file (plus its transitive imports) compiles, regardless of include.
  10. Comments in tsconfig.json — TS 1.7+ supports JSONC (JSON with comments), but some external tools (older prettier, some linters) reject them. Use tsconfig.json for the file but author with // ... comments freely — tsc is happy.

Real-world recipes

Strict tsconfig for a new project

The baseline that catches the most bugs without being unreasonable. Drop this into a new project and tune from there.

json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": ["ES2022"],
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitReturns": true,
    "noImplicitOverride": true,
    "noFallthroughCasesInSwitch": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "outDir": "dist",
    "rootDir": "src",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "esModuleInterop": true,
    "verbatimModuleSyntax": true,
    "isolatedModules": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
bash
npx tsc --noEmit

Output:

text
(no output — exit code 0)

Bun + TypeScript + Vite project

Bun reads tsconfig.json natively. For a Vite-based front-end built with Bun, the config looks slightly different — moduleResolution: "bundler", no emit, JSX preserved for Vite to transform.

json
{
  "extends": "@tsconfig/strictest/tsconfig.json",
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "jsx": "react-jsx",
    "noEmit": true,
    "allowImportingTsExtensions": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    },
    "types": ["@types/bun", "vite/client"]
  },
  "include": ["src"]
}
bash
bun add -d typescript @tsconfig/strictest @types/bun

Output:

text
bun add v1.2.18

 installed typescript@5.4.5
 installed @tsconfig/strictest@2.0.5
 installed @types/bun@1.1.5

 3 packages installed [127.00ms]

Cloudflare Workers tsconfig

Cloudflare Workers run on V8 isolates — no Node, no DOM, just fetch-style request handling. Use the @cloudflare/workers-types package for Worker globals.

json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "lib": ["ES2022", "WebWorker"],
    "types": ["@cloudflare/workers-types"],
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "skipLibCheck": true,
    "noEmit": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true
  },
  "include": ["src/**/*", "worker-configuration.d.ts"]
}
bash
npx tsc --noEmit
npx wrangler deploy

Output:

text
Uploaded my-worker (1.42 sec)
Published my-worker (0.32 sec)
  https://my-worker.example.workers.dev

Split tsconfigs for source + tests

Tests usually need different lib/types than source — @types/jest, vitest/globals, @types/node. Split into two configs and use extends.

json
// tsconfig.json — source
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "strict": true,
    "outDir": "dist",
    "rootDir": "src",
    "skipLibCheck": true
  },
  "include": ["src/**/*"],
  "exclude": ["**/*.test.ts", "**/*.spec.ts"]
}
json
// tsconfig.test.json — tests
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "noEmit": true,
    "types": ["vitest/globals", "@types/node"]
  },
  "include": ["src/**/*.test.ts", "src/**/*.spec.ts"]
}
bash
npx tsc --noEmit -p tsconfig.test.json

Output:

text
(no output — exit code 0)

The test config inherits everything from the main config, adds Vitest globals, and excludes nothing — it picks up tests that tsconfig.json skips.

Migrate a JavaScript project incrementally

Existing JS codebase, no rewrite. Use allowJs: true + checkJs: false to ship without errors; flip checkJs: true per-file with // @ts-check.

json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "allowJs": true,
    "checkJs": false,
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"]
}

In any .js file, add the magic comment to opt in:

javascript
// @ts-check
/**
 * @param {string} name
 * @returns {string}
 */
function greet(name) {
  return `Hello, ${name}!`;
}

tsc --noEmit now type-checks just this file using JSDoc annotations. See project references for per-directory migration.

Verify what your tsconfig actually does

Three commands answer "is my config doing what I think":

bash
# 1. After extends resolution and glob expansion
npx tsc --showConfig

# 2. Every file the compiler will read
npx tsc --listFiles --noEmit | wc -l

# 3. Per-pass timing
npx tsc --diagnostics --noEmit

Output:

text
{ "compilerOptions": { ... }, "include": [ ... ] }
247
Files:                          47
Lines:                       18421
Parse time:                   0.42s
Bind time:                    0.12s
Check time:                   1.87s
Total time:                   2.41s

If the file count is way higher than expected, check include/exclude and consider tightening types. If check time dominates, look at generic-heavy hot spots.