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,modulekeyed offtarget), even thoughtsc --initscaffolds 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: setstrict,target,module, andlibexplicitly so behavior doesn’t shift under you across versions.
Core Concepts
Top-level fields (brief — deep dives are deferred)
extends— inherit another config: a relative path or a package, e.g."@tsconfig/node20/tsconfig.json". Accepts an array (TS 5.0+) merged left→right. The child wins on conflicts;files/include/excludefrom the parent are overwritten (not merged) if redefined. Relativepaths/outDirin the parent resolve against the parent’s location.files— an explicit allowlist of files. No globs. Best for tiny projects; otherwise useinclude.include— glob patterns ("src/**/*"). If omitted, defaults to everything under the config dir (minusexclude).*/?/**supported; patterns without an extension match.ts/.tsx/.d.ts(and.js/.jsxwhenallowJs).exclude— globs removed frominclude(defaults tonode_modules,bower_components,jspm_packages, andoutDir).excludeonly filtersinclude; it does not stop a file pulled in by animportor a/// <reference>.references— array of{ "path": "../pkg" }for project references (composite builds). Deep coverage →typescript-project-references-monorepo.
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
strictfamily is these eight per the TSConfig reference. Newer TS lines have floated additionalstrict-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:
"commonjs"—require/module.exportsoutput. Legacy Node / CJS packages."node16"/"nodenext"— emit format is chosen per file from the nearestpackage.json"type"(and.mts/.ctsextension). The correct choice for code that runs in modern Node.nodenexttracks the latest Node behavior;node16pins to the Node 16 semantics."esnext"/"es2015"/"es2020"/"es2022"— pure ESM output at the stated level. Use for code a bundler will consume, or pure-ESM libraries."preserve"(TS 5.4) — leave imports/import()exactly as written, no rewriting. ImpliesmoduleResolution: bundler. The modern “I’m handing this to a bundler” choice.- (Deprecated/legacy:
amd,umd,system,none.)
moduleResolution — how a specifier maps to a file:
"node10"(the option formerly named"node") — classic Node CJS resolution. Noexports/importsfield support. Legacy only."node16"/"nodenext"— modern Node resolution honoringpackage.json"exports"/"imports", conditional exports, and.mts/.cts. Pair withmodule: node16/nodenext. Required for correctly typing dual-format packages."bundler"(TS 5.0) — models esbuild/Vite/webpack/Parcel: extensionless imports allowed (like CJS) but prefersimportconditions inexports(like ESM). For app code consumed by a bundler. Not for published libraries — it hides resolution problems your consumers (who may not bundle) would hit; ship withnode16/nodenextinstead."classic"— pre-Node TS resolution. Effectively never use it. (Resolution algorithm internals +exports/importsmechanics →nodejs-module-resolution.)
target — the ECMAScript version tsc downlevels syntax to (e.g. es2015…es2023, 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:
baseUrl— base directory for resolving bare specifiers. (Deprecated in TS 6.0; preferpathswithout it, or packageimports.)paths— remap import specifiers to locations, e.g.{ "@app/*": ["./src/*"] }.tscand editors honor these for type-checking only — they do not rewrite emitted paths. A bundler,tsc-alias, orpackage.json"imports"must make them work at runtime. Node’s native runner ignorespaths.rootDir— the input root that mirrors intooutDir(controls output folder structure).rootDirsmerges multiple virtual roots into one.outDir— where emitted.js/.d.tsgo.resolveJsonModule— allowimport data from "./x.json"with an inferred type. Requires amodulethat supports it (most do except some legacy modes).allowImportingTsExtensions(5.0) — permitimport "./x.ts"(explicit TS extension). Only allowed withnoEmitoremitDeclarationOnly(sincetsccan’t emit a.tsimport). Needed for Node’s native type-stripping workflow.moduleDetection—"auto"(default; a file with anyimport/export, or undermodule: node16/nodenextwith"type":"module", is a module),"force"(treat every file as a module — recommended to avoid global-scope surprises),"legacy".resolvePackageJsonExports/resolvePackageJsonImports— consult thepackage.json"exports"/"imports"fields. Defaulttrueundernode16/nodenext/bundler. (Field mechanics →nodejs-module-resolution.)
Emit & interop
declaration— emit.d.tsfiles. Mandatory for a published library. Defaultstruewhencompositeis on, elsefalse.declarationMap— emit.d.ts.mapso consumers’ “go to definition” jumps to your.tssource, not the.d.ts. Ship for libraries with source.sourceMap— emit.js.mapfor runtime debugging.noEmit— type-check only, produce no files. The standard setting when a bundler (or Node’s strip-types) does the actual transpile andtscis the type gate.isolatedModules— guarantee every file can be transpiled alone, without cross-file type info (which is exactly how esbuild/swc/Babel/Node-strip operate). It bans constructs needing whole-program knowledge — re-exporting a type withoutexport type,const enum, certain namespace patterns. Turn it on whenever a single-file transpiler is in the pipeline.verbatimModuleSyntax(5.0) — leave any import/export without atypemodifier exactly as written, and drop anything withtype. Replaces the deprecatedimportsNotUsedAsValues+preserveValueImports. Makes value-vs-type imports explicit and prevents accidental elision — pair it withisolatedModules/erasableSyntaxOnly. Caveat: it won’t rewrite ESM syntax torequire, so don’t combine it withmodule: commonjsif you writeimport/export.esModuleInterop— generate interop helpers soimport express from "express"works against a CJS module without a real default export. ImpliesallowSyntheticDefaultImports. Effectively always-on in modern configs (and undisablable in TS 6.0).allowSyntheticDefaultImports— allow default-style imports from modules lacking a default export, for type-checking only (no emit change). Implied byesModuleInterop.erasableSyntaxOnly(5.8) — error on TS constructs that emit runtime code:enum,namespacewith runtime members, parameter properties (constructor(private x)),import =. Mirrors exactly what Node’s native type-stripping refuses, so the editor catches the mismatch instead of a runtime crash. Pair withverbatimModuleSyntax. (Node runtime side →nodejs-typescript-and-runtime-features.)
JS interop & JSX
allowJs— let.js/.jsxfiles into the program (imported by, or alongside,.ts). Needed for incremental migration and for emitting from a JS codebase.checkJs— type-check those.jsfiles (using JSDoc annotations). Per-file opt-in/out via// @ts-check/// @ts-nocheck. RequiresallowJs.jsx— JSX transform:"preserve"(emit.jsx, leave JSX for a bundler),"react"(classicReact.createElement),"react-jsx"(the automatic runtime — noimport Reactneeded; the modern default for React 17+),"react-jsxdev","react-native".jsxImportSource(4.1) — the module the automatic runtime importsjsx/jsxsfrom (default"react"); set to"preact","@emotion/react", etc. Only meaningful withjsx: react-jsx/react-jsxdev.
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:
strictnow defaultstrue(wasfalse). The notes are explicit: “If you were relying on the previous default offalse, you’ll need to explicitly set"strict": falsein yourtsconfig.json.”moduledefaultsesnext(ESM is now the dominant format).targetdefaults to the most recent supported ECMAScript spec — a floating target, currentlyes2025.typesdefaults[](no longer auto-pulls every installed@typespackage — declare what you need).rootDirdefaults to the directory containingtsconfig.json.noUncheckedSideEffectImportsdefaultstrue(already true on the 5.6+ reference);libReplacementnow defaultsfalsefor performance.
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
tsc --init— scaffold a commentedtsconfig.json. The generated defaults have grown stricter over versions; treat the output as a starting point, not gospel — prune comments and pin the four module/target options.@tsconfig/bases— official community base configs youextends:@tsconfig/node20,@tsconfig/node22,@tsconfig/strictest,@tsconfig/recommended, framework bases (@tsconfig/vite-react, etc.). Inherit one and override the few project-specific keys.tsc --showConfig— print the fully-resolved config (afterextendsmerging and defaults). The fastest way to answer “what is actually in effect here?”tsc --explainFiles/--listFilesOnly— show why each file is in the program (whichinclude/import/referencepulled it in). Use wheninclude/excludeisn’t behaving.tsc --noEmit— the type-check gate to run in CI when a bundler or Node strip-types does the real build.tsc-alias/ bundlerresolve.alias— makepathswork at runtime (sincetscdoesn’t rewrite them).
Methodology
- Inherit, don’t hand-roll. Start from
@tsconfig/node22(or a framework base) viaextends, then override only what’s project-specific. - Set the module quartet together by runtime target: Node →
module: nodenext+moduleResolution: nodenext; bundler/web →module: preserve(impliesbundler) +noEmit; library →module: nodenext(oresnext) +declaration: true. - Turn on
strict(default in 6.0) and, for new code, the high-value standalone checksnoUncheckedIndexedAccess+noImplicitOverride. AddexactOptionalPropertyTypes/@tsconfig/strictestonly if the team will pay the friction. - Pin
targetandlibexplicitly (e.g.es2022;["es2022"]server vs["es2022","dom","dom.iterable"]web) so the 6.0 default flips don’t silently change behavior. - If a single-file transpiler is in the pipeline (esbuild/swc/Vite/Node strip-types), set
isolatedModules: true+verbatimModuleSyntax: true(+erasableSyntaxOnlyfor the native-Node path). - Verify with
tsc --showConfigand atsc --noEmitrun 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
- Mixing module modes.
module: esnextwithmoduleResolution: node10(orbundlerwithmodule: commonjs) gives wrong resolution/emit. Keepmodule/moduleResolutionconsistent with the runtime. moduleResolution: bundlerin a published package. It validates only the bundler case and hides breakage for consumers using plain Node resolution. Libraries →node16/nodenext.- Expecting
pathsto work at runtime.tscnever rewrites them; without a bundler/tsc-alias/packageimports, the emitted JS has unresolved bare specifiers. - Leaving
target: ES5(5.x default) unset on a modern runtime — bloated downlevel output and missing lib types. Always pintarget. enum/namespace/parameter-properties underisolatedModulesor a strip-types runtime — they need whole-program emit; useerasableSyntaxOnlyto catch them at author time.skipLibCheckeverywhere as a crutch — it’s a performance win, but it can mask genuine conflicts between@typespackages; don’t reach for it to silence a real type error.- Re-exporting a type without
export typewhenisolatedModules/verbatimModuleSyntaxis on — a single-file transpiler can’t tell it’s type-only and may emit a broken value import. - Relying on TS 6.0’s default flips. Be explicit about
strict/target/module/libso a 5.x and a 6.0 toolchain produce identical results.
Troubleshooting
- “Cannot use import statement outside a module” / wrong require-vs-import emit →
module/moduleResolutiondon’t match the runtime; switch tonodenextand check the package’s"type". pathsimport fails at runtime (works in editor) → expected; wiretsc-alias, a bundler alias, orpackage.json"imports".- “This import path can only be used with allowImportingTsExtensions” → you imported
./x.ts; setallowImportingTsExtensions: true(andnoEmit/emitDeclarationOnly). - “
Xis declared but never used” / unexpected unused errors → a@tsconfig/strictestbase enablednoUnusedLocals/noUnusedParameters; relax or prefix with_. enum/parameter-property errors under a no-emit setup →erasableSyntaxOnlyis on (or the runtime strips types); rewrite to erasable constructs.- Config changes seem ignored → run
tsc --showConfig; anextendsparent or an editor pinning a differenttsconfigis overriding you. - A file you expected isn’t compiled → it’s outside
files/includeor hitexclude;excludecan’t remove a file reached viaimport. Usetsc --explainFiles. - New type errors after a TS upgrade →
strictmay gate additional checks in the new line (and 6.0 flips defaults); pin versions and read that release’s notes. - DOM globals (
document,window) missing or unexpectedly present → setlibexplicitly (include/excludedom).
References
- TypeScript — TSConfig Reference (every option, defaults): https://www.typescriptlang.org/tsconfig/
- TypeScript Handbook — What is a tsconfig.json: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html
- TypeScript Handbook — Modules: Choosing Compiler Options: https://www.typescriptlang.org/docs/handbook/modules/guides/choosing-compiler-options.html
- TypeScript 5.0 release notes (
verbatimModuleSyntax,bundler,allowImportingTsExtensions): https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html - TypeScript 5.4 release notes (
module: preserve): https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-4.html - TypeScript 5.6 release notes (
noUncheckedSideEffectImports): https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-6.html - TypeScript 5.8 release notes (
erasableSyntaxOnly,rewriteRelativeImportExtensions): https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-8.html - TypeScript 6.0 release notes (default flips, deprecations, last JS-based release): https://www.typescriptlang.org/docs/handbook/release-notes/typescript-6-0.html
@tsconfig/bases(community base configs): https://github.com/tsconfig/bases- Total TypeScript — The TSConfig Cheat Sheet (Matt Pocock baselines): https://www.totaltypescript.com/tsconfig-cheat-sheet