tsconfig, Strictness & Project References
In one line
strict: true is the baseline, three flags outside it close the biggest remaining holes, and project references are what keep type-checking fast once one package becomes several.
What it is
strict is a bundle, and the two members that matter most are strictNullChecks — which separates null and undefined from every other type and is the single largest safety win TypeScript offers — and noImplicitAny, which forces you to say when you are opting out. Turning strict off to make errors go away is the decision that produces a codebase where the types are decorative.
Three flags outside the bundle are worth enabling deliberately:
noUncheckedIndexedAccess adds | undefined to index signature and array lookups. arr[0] becoming possibly-undefined is annoying and correct — it is the flag that catches the empty-array crash.
exactOptionalPropertyTypes distinguishes an absent property from one explicitly set to undefined, which matters whenever you spread partial objects over defaults.
verbatimModuleSyntax makes import elision predictable and forces import type for type-only imports — necessary once bundler and runtime disagree about what a side-effect import means.
Then the resolution settings that determine whether modern packages work at all: moduleResolution: "bundler" for anything built by Vite, webpack, or Next; "nodenext" for Node libraries. Getting this wrong is the usual cause of "cannot find module" against a package with an exports map. tsconfig/bases provides sensible presets rather than assembling one from memory.
Project references address scale. Once a repo has several packages, one big tsc re-checks everything on every change. References split it: each package has its own config with composite: true, declares what it depends on, and emits .d.ts plus a build-info file so tsc --build recompiles only what changed. Consumers type-check against the emitted declarations rather than the source, which is what makes it fast — and also what makes a stale build produce confusing errors.
Two operational notes. Editors use the workspace's TypeScript version, not the global one — pin it per project or two developers see different errors from the same code. And tsc --noEmit in CI is not optional: the bundler strips types without checking them, so a build passing proves nothing about type correctness.
Why it matters
Strictness settings determine whether TypeScript prevents bugs or just annotates them, and the three flags outside strict are where most remaining runtime type errors come from.
Migration questions — "how would you turn on strict in a large codebase?" — are common, and the answer is incremental: enable per-directory with overrides, or use strict with targeted // @ts-expect-error and a burn-down list.
Key points
strict: trueis the baseline;strictNullChecksalone eliminates the largest class of runtime type errors.- Add
noUncheckedIndexedAccess— array and record lookups really can be undefined. exactOptionalPropertyTypesseparates missing from explicitly-undefined, which matters when spreading.verbatimModuleSyntaxmakes type-only imports explicit and import elision predictable.- Set
moduleResolutiontobundlerornodenextto match reality, or modernexportsmaps will not resolve. - Project references with
compositemake large repos incrementally checkable viatsc --build. - Pin the editor's TypeScript version, and run
tsc --noEmitin CI — bundlers strip types without checking them.