TypeScript Compiler Configuration

TypeScript Compiler Configuration — tsconfig.json & compilerOptions

A lang-js-ts reference for the tsconfig.json file and the full compilerOptions surface. The goal: pick a correct, version-appropriate config the first time, know what each strictness flag costs, and copy a sane baseline for a Node app, a bundler/web app, or a published library. Defer module resolution algorithm internals, project references / tsc -b, and external bundler config to the siblings listed in the provenance block.

Overview

tsconfig.json marks a directory as the root of a TypeScript project and tells tsc (and every editor, bundler plugin, and ts-node/tsx) what files to compile and under what rules. Running tsc with no input files makes it search up from the CWD for the nearest tsconfig.json; tsc -p ./path points at a specific one. The shape is two halves: a small set of top-level fields (which files, what to extend) and the large compilerOptions object (how to type-check, resolve, and emit).

Version anchor (memorize — these drive “is this flag available / on” questions):

Flag / change Landed in Note
noUncheckedIndexedAccess, jsxImportSource TS 4.1
noImplicitOverride TS 4.3
useUnknownInCatchVariables, exactOptionalPropertyTypes TS 4.4 useUnknownInCatch… is in the strict family
moduleResolution: "bundler", verbatimModuleSyntax, allowImportingTsExtensions, allowArbitraryExtensions TS 5.0 verbatimModuleSyntax replaces importsNotUsedAsValues + preserveValueImports
module: "preserve" TS 5.4 implies moduleResolution: bundler; emits ESM imports as-is and import …= require() as require()
noUncheckedSideEffectImports TS 5.6 defaults true
erasableSyntaxOnly, rewriteRelativeImportExtensions TS 5.8 aligns tsc with Node’s native type-stripping
Default flips (strict, module, target, types, rootDir), big deprecations TS 6.0 (Mar 2026) last JS-based release before the Go-based TS 7.0; see “TS 6.0 delta”

Defaults vs tsc --init. Through TS 5.x the compiler defaults are permissive (strict: false, target: ES5, module keyed off target), even though tsc --init scaffolds a strict-on file — “default” in this doc means the compiler default for the stated version line, not what a generated file shows. TS 6.0 changes the compiler defaults themselves (see the TS 6.0 delta). The robust habit either way: set strict, target, module, and lib explicitly so behavior doesn’t shift under you across versions.

Core Concepts

Top-level fields (brief — deep dives are deferred)

Type-checking / strictness

strict is a bundle switch. Setting "strict": true turns on all eight family members at once; you can then re-disable any single one ("strict": true, "strictNullChecks": false) — the explicit flag overrides the bundle. The eight strict-family flags:

Flag What it does
noImplicitAny Error when an expression/declaration falls back to an inferred any (e.g. an untyped parameter).
strictNullChecks null/undefined are distinct types, not assignable to everything. The single most valuable flag — enable it before the others if migrating.
strictFunctionTypes Function parameters checked contravariantly (sound) instead of bivariantly. Does not apply to method syntax on interfaces/classes.
strictBindCallApply .call/.apply/.bind are type-checked against the function’s real parameters.
strictPropertyInitialization Class fields must be initialized in the constructor or marked ?/!. Requires strictNullChecks to take effect.
noImplicitThis Error on a this whose type is an implied any.
useUnknownInCatchVariables catch (e) types e as unknown instead of any, forcing a narrowing check (TS 4.4).
alwaysStrict Parse every file in ECMAScript strict mode and emit "use strict".

The exact membership of the strict family is these eight per the TSConfig reference. Newer TS lines have floated additional strict-gated checks; verify against the reference for your version before relying on one beyond these eight.

Standalone checks (NOT enabled by strict — opt in individually):

Flag Since What it does / cost
noUncheckedIndexedAccess 4.1 Adds | undefined to any index-signature/array access (arr[i], rec[key]). High value, high friction — forces a guard or ! on every dynamic access.
exactOptionalPropertyTypes 4.4 { x?: T } means “absent or T” — assigning undefined explicitly is an error. Surfaces real present-vs-absent bugs; noisy with libraries that pass undefined.
noImplicitOverride 4.3 Require the override keyword when a subclass method overrides a base method. Prevents silent signature drift.
noFallthroughCasesInSwitch Error on a non-empty case that falls through without break/return/throw.
noUncheckedSideEffectImports 5.6 Error if a side-effect-only import (import "./x") doesn’t resolve to a real file. Defaults true (low-risk: only affects bare side-effect imports).
noPropertyAccessFromIndexSignature Force bracket access (obj["key"]) for properties that only exist via an index signature; reserve dot access for declared properties.
noImplicitReturns Every code path in a function with a return type must return a value.
allowUnreachableCode false errors on unreachable code; undefined (default) warns; true silences. (allowUnusedLabels is the sibling for labels.)

The @tsconfig/strictest preset enables the full set above (plus noUnusedLocals, noUnusedParameters, noImplicitReturns, etc.) for green-field projects that can afford it.

Modules

These four options are interdependent — set them as a group, not piecemeal.

module — the module format tsc emits and the import syntax it understands:

moduleResolutionhow a specifier maps to a file:

target — the ECMAScript version tsc downlevels syntax to (e.g. es2015es2023, esnext). Drives the default lib and the default module. Through 5.x the default is ES5; pick at least es2022 for modern runtimes (top-level await, class fields, Error.cause).

lib — which built-in type declarations to include (e.g. ["es2022", "dom", "dom.iterable"]). Omitting it derives a set from target. Set it explicitly to control DOM availability: include dom for browser code, omit it for pure Node/server code so document/window don’t type-check.

Path mapping & related:

Emit & interop

JS interop & JSX

The TS 6.0 delta (current as of June 2026)

TypeScript 6.0 (released March 2026) is the last JavaScript-based release before the Go-based TS 7.0 (“native”). Per the official handbook release notes it changes compiler defaults — relevant to this skill:

It also adds --stableTypeOrdering (to diff 6.0 vs 7.0 output) and deprecates/removes legacy surface (target: es5 + --downlevelIteration, moduleResolution: node10/classic, module: amd/umd/system/none, --outFile, baseUrl). Practical takeaway: explicitly set strict, target, module, and lib in your config so behavior is identical across 5.x and 6.0 instead of relying on defaults that shifted.

Tools / Frameworks

Methodology

  1. Inherit, don’t hand-roll. Start from @tsconfig/node22 (or a framework base) via extends, then override only what’s project-specific.
  2. Set the module quartet together by runtime target: Node → module: nodenext + moduleResolution: nodenext; bundler/web → module: preserve (implies bundler) + noEmit; library → module: nodenext (or esnext) + declaration: true.
  3. Turn on strict (default in 6.0) and, for new code, the high-value standalone checks noUncheckedIndexedAccess + noImplicitOverride. Add exactOptionalPropertyTypes/@tsconfig/strictest only if the team will pay the friction.
  4. Pin target and lib explicitly (e.g. es2022; ["es2022"] server vs ["es2022","dom","dom.iterable"] web) so the 6.0 default flips don’t silently change behavior.
  5. If a single-file transpiler is in the pipeline (esbuild/swc/Vite/Node strip-types), set isolatedModules: true + verbatimModuleSyntax: true (+ erasableSyntaxOnly for the native-Node path).
  6. Verify with tsc --showConfig and a tsc --noEmit run before trusting the file.

Practical Patterns

Node app (TS 5.x/6.0, transpiled by tsc):

{
  "extends": "@tsconfig/node22/tsconfig.json",
  "compilerOptions": {
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "target": "es2023",
    "lib": ["es2023"],
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "esModuleInterop": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true,
    "resolveJsonModule": true,
    "moduleDetection": "force",
    "skipLibCheck": true,
    "rootDir": "src",
    "outDir": "dist",
    "sourceMap": true,
    "declaration": false
  },
  "include": ["src/**/*"],
  "exclude": ["dist", "node_modules"]
}

Bundler / web app (Vite/esbuild/webpack do the transpile; tsc is the type gate):

{
  "compilerOptions": {
    "module": "preserve",
    "noEmit": true,
    "target": "es2022",
    "lib": ["es2022", "dom", "dom.iterable"],
    "jsx": "react-jsx",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "esModuleInterop": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true,
    "resolveJsonModule": true,
    "moduleDetection": "force",
    "skipLibCheck": true,
    "allowImportingTsExtensions": true,
    "paths": { "@/*": ["./src/*"] }
  },
  "include": ["src"]
}

Published library (dual-friendly types, source-mapped declarations):

{
  "extends": "@tsconfig/node20/tsconfig.json",
  "compilerOptions": {
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "target": "es2021",
    "lib": ["es2021"],
    "strict": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true,
    "rootDir": "src",
    "outDir": "dist",
    "skipLibCheck": true
  },
  "include": ["src"],
  "exclude": ["**/*.test.ts"]
}

Node native type-stripping (zero build; tsc --noEmit only validates):

{
  "compilerOptions": {
    "noEmit": true,
    "module": "nodenext",
    "target": "esnext",
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "rewriteRelativeImportExtensions": true,
    "allowImportingTsExtensions": true
  }
}

(Native-runtime behavior itself → nodejs-typescript-and-runtime-features.)

Anti-Patterns

Troubleshooting

References