tsconfig.json: TypeScript Compiler Settings Explained
The tsconfig.json file decides what "TypeScript says it is fine" actually means in your project. It sets which files are checked, how suspicious the checker is, how imports resolve, and what JavaScript comes out. Two projects with the same code and different configs can report different type errors. For the surrounding setup, see TypeScript tooling; for language basics, start with the TypeScript guide.
What Does tsconfig.json Control?
A tsconfig.json file controls source files, type-checking rules, module resolution, and JavaScript output. This example suits an application whose bundler produces the JavaScript:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"noEmit": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
} Three groups are doing different work here, and mixing them up is the usual source of copied-config trouble. strict and the checks beneath it are the type-checking rules, and they travel well between projects. target, module, and moduleResolution describe the runtime and bundler, so they do not. include and exclude decide the program's file set.
noEmit is worth calling out: it says this config only checks types and writes no JavaScript, which is the normal arrangement when a bundler or framework handles the build. Leave it off for a package that tsc itself compiles.
Which Files Does TypeScript Check?
TypeScript checks files selected by include and files, plus the files they import. exclude removes matches from include, but does not override explicit files or imports. For example, an imported file can still be checked even when its folder is excluded:
{
"include": ["src"],
"exclude": ["src/legacy"]
} If src/app.ts imports src/legacy/parser.ts, the excluded file is still added to the program and still checked, because the compiler cannot type the importer without it. exclude only removes files from the initial discovery pass.
Configuration is also per directory, not global. Running tsc without a file argument finds the nearest tsconfig.json by walking up from the current directory. Since TypeScript 6, passing filenames on the command line while a tsconfig.json is present is an error, and tsc --ignoreConfig src/app.ts is how you ask for the old defaults-only behavior, which is why such a command can report completely different errors from the CI failure you were reproducing.
What Does TypeScript Strict Mode Check?
"strict": true enables TypeScript checks for implicit any, null and undefined values, function compatibility, and class property initialization, among others. These are the checks most likely to affect your code:
noImplicitAnyreports parameters and variables the compiler cannot infer, instead of falling back toany.strictNullCheckskeepsnullandundefinedout of other types unless declared, so each becomes a distinct type you narrow before use.strictFunctionTypeschecks function-typed parameters contravariantly, catching some unsound callback assignments.strictPropertyInitializationrequires class fields to be assigned in the constructor or by an initializer, unless the type includesundefinedor carries a definite-assignment assertion.strictBindCallApplytypesbind,call, andapplyagainst the real signature.useUnknownInCatchVariablestypes a caught error asunknownrather thanany, so you narrow before using it.
Each can be set individually, which is what makes a gradual migration possible: turn strict on and switch one member back off while its errors are worked through, rather than leaving the whole family disabled. Note that the exact membership of the group has grown across releases, so on an older compiler version some of these are separate opt-ins.
Flags You Add Deliberately
Several of the most useful checks are not part of strict and stay off until you name them:
const tags = ["typescript", "types"];
const first = tags[0];
const third = tags[2];
console.log(third.toUpperCase()); // with noUncheckedIndexedAccess enabled, tsc reports:
// error TS18048: 'third' is possibly 'undefined'. Without the flag, tags[2] is typed string even though the array has two elements, and the mistake reaches runtime as a TypeError. With it, unchecked array and index-signature lookups yield Type | undefined, while declared properties and statically valid tuple positions stay definite. That catches missing elements, but requires more checks in code that frequently uses indexes.
exactOptionalPropertyTypes is the other one worth knowing by name. It separates "the property is absent" from "the property is present and set to undefined", so an optional field no longer silently accepts an explicit undefined. That distinction matters most for patch-style updates, where the two cases mean different things.
Beyond those, noFallthroughCasesInSwitch, noImplicitOverride, and noImplicitReturns each catch a specific class of mistake at low cost. There is no need to adopt everything at once. Add one, fix what it reports, and keep it.
Module, Target, and Emit Settings
These describe the world the code runs in, so they are the settings least safe to copy between projects. target sets the JavaScript version the compiler emits and, with it, which library types are available by default. module sets the module format of the output, and moduleResolution tells the compiler how to find an import specifier.
The pairing matters more than either value alone: "moduleResolution": "Bundler" is intended for code processed by a bundler and works with ESNext or Preserve, and TypeScript 6 also supports pairing it with CommonJS, while a Node package resolving its own imports wants a Node-oriented mode instead. A mismatch usually shows up as an import that resolves in the editor and fails in the build, or the reverse.
Two smaller options save real time. skipLibCheck stops the compiler type checking inside declaration files, which can speed up large projects. isolatedModules is the general check that every file can be transpiled on its own, while verbatimModuleSyntax controls import and export elision and rejects module syntax that would need incompatible rewriting. Between them they keep a fast build tool and a full type check agreeing with each other.
Extending and Splitting Configs
In a workspace, put the shared half in one file and let each package extend it:
// tsconfig.base.json
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"skipLibCheck": true
}
}
// packages/api/tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist"
},
"include": ["src"]
} The package config inherits the strictness settings and sets only what is local. Options given in the extending file replace the inherited value rather than merging with it, and relative paths in an inherited config resolve from the file that declared them, which is the usual explanation for an outDir landing somewhere unexpected.
For larger workspaces, project references (references plus composite) let tsc --build check packages in dependency order and reuse previous results. They add setup cost, so they pay off once builds are slow enough to notice rather than on day one.
Pitfalls and Debugging
The editor and the build disagree. Usually two different configs or two different compiler versions. Editors typically use the nearest config to the open file, and may run their own bundled TypeScript unless told to use the workspace version. Confirm both before hunting for a compiler bug.
A copied config that does not match the runtime. The strictness half of someone else's config is usually safe to adopt; the module and target half describes their bundler and runtime. Copying both is how a Node service ends up with browser-oriented resolution.
Assuming the config runs anywhere. tsconfig.json configures type checking and tsc. A bundler that strips types with esbuild or SWC reads only some of it, and enforces none of the checking, which the workflow guide covers.
Turning strict on across a large codebase at once. Thousands of errors nobody triages tends to end with the flag being reverted. Enable it, disable one member such as strictNullChecks temporarily, and re-enable it once its errors are worked through. Flags apply to a whole configured program, so staging by directory needs separate configs or project references.
Frequently Asked Questions
Does TypeScript strict mode enable every strictness check?
TypeScript strict mode does not enable every strictness check. It enables a group of checks, including noImplicitAny, strictNullChecks, strictFunctionTypes, and strictPropertyInitialization. Several stricter options sit outside that group and stay off unless you name them, notably noUncheckedIndexedAccess and exactOptionalPropertyTypes.
Why does TypeScript check files listed in exclude?
TypeScript can check a file listed in exclude because exclude only filters files discovered through include. If any included file imports the excluded one, it is pulled into the program anyway, since the compiler cannot check the importer without it. Stopping that means removing the import or splitting the code into a separate project.
Should you enable TypeScript skipLibCheck?
Enable skipLibCheck if faster checks are worth skipping errors inside declaration files. It can reduce checking time and suppress errors within third-party declarations. The cost is that a genuine conflict between two library versions can go unreported, so library authors are more cautious about it.
Can one tsconfig serve a whole monorepo?
One tsconfig.json can cover a monorepo, but a shared base plus a config per package is often easier to maintain, because packages differ in module settings, JSX, and target even when their strictness matches. The extends field keeps the shared half in one file, and project references let the compiler build packages in dependency order.
Sources
-
[1]
TSConfig Reference(typescriptlang.org)
-
[2]
What is a tsconfig.json(typescriptlang.org)
-
[3]
Project References(typescriptlang.org)
Read Next
The compiler commands worth knowing, why transpiling is not type checking, what the editor language service does, wiring a real check into CI, and migrating a JavaScript codebase.
JSX settings, component prop types, children and native element props, typed state and refs, event handlers, and what types cannot enforce across the server and client boundary.
The half of TypeScript that is not types: tsconfig.json, the split between transpiling and type checking, React components, and API contracts between a frontend and its server.