Skip to content

TypeScript Config Deep Dive: Strict Mode and the Toolchain

Which strict flags change day-to-day React and Node work, what breaks when you flip them, and how tsc, the editor, and the bundler each read your tsconfig.

5 min read
TypeScript
tsconfig
strict
noUncheckedIndexedAccess
Toolchain

After a file-by-file leap to TypeScript, teams usually inherit a tsconfig that "works" and a vague mandate to "be strict." Strict is not one switch with one personality. It is a bundle of checks, plus a few optional flags that hurt more than the bundle, and a toolchain that does not always honor the same file the same way.

What strict: true actually turns on

As of the TypeScript 5.x line I am writing against, strict enables a set of flags including (among others) strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitAny, noImplicitThis, and useUnknownInCatchVariables. Exact membership can shift across majors, read your version's docs when upgrading, but the day-to-day pain is predictable.

Flag people feelWhat breaks firstWhen I insist
strictNullChecksOptional props treated as always present; API JSON assumed completeAlways for app code
noImplicitAnyUntyped parameters, empty callbacks, JSON.parse resultsAlways
strictFunctionTypesCallback variance bugs in props and event handlersAlways
strictPropertyInitializationClass fields set in componentDidMount-era patternsAlways; prefer definite assignment sparingly
useUnknownInCatchVariablescatch (e) used as e.message without narrowingAlways

Turning strict off to "finish the migration" is how six months later nobody remembers which holes were intentional.

Extras that are not in strict (and bite harder)

These are the ones I schedule deliberately after the leap:

noUncheckedIndexedAccess

ts
const map: Record<string, number> = { a: 1 }; const value = map['b']; // without flag: number // with flag: number | undefined

Arrays and index signatures suddenly return T | undefined. That is correct and annoying. React lists that did items[i].id without a guard light up.

When I flip it: shared domain packages and anything touching money, permissions, or routing params. When I delay: greenfield UI spikes where the noise delays shipping and tests already cover the empty cases. I do not leave it off forever in product cores.

exactOptionalPropertyTypes

ts
type Props = { title?: string }; // With the flag, `{ title: undefined }` is not the same as `{}`. const a: Props = {}; // ok const b: Props = { title: undefined }; // error under exactOptionalPropertyTypes

This catches real bugs when optional means "omit" but callers pass explicit undefined from spreads. It also fights common React patterns like

ts
<Button {...(disabled ? { disabled: true } : {})} />

versus spreading { disabled: maybeUndefined }.

When I flip it: API client types and shared component libraries. When I wait: app folders mid-migration with heavy prop spreading.

noPropertyAccessFromIndexSignature

Forces obj['key'] for index signatures instead of obj.key. Good for dictionary types; noisy for loosely typed config objects. I enable it in packages where "stringly keys" are the product.

Toolchain: three readers of one config

A surprising amount of "TypeScript is wrong" is three tools disagreeing.

  1. tsc --noEmit in CI: the source of truth for the repo's type gate. If CI is green and the editor is red, trust CI first, then fix editor project loading.
  2. The editor (TS language service): may load a different tsconfig via solution-style roots, a nested package config, or a stale server. "Restart TS server" is not superstition; wrong project graph is common in monorepos.
  3. The bundler (Vite, etc.): uses esbuild/SWC for transpile. It does not enforce your strict flags. Shipping can succeed while tsc fails, or the reverse if you skip tsc in CI.

So: bundler for speed, tsc for truth. Dropping the CI typecheck because "Vite built fine" is how any returns through the side door.

Project references and paths without self-harm

Monorepo sketch that stays honest:

json
// apps/web/tsconfig.json { "extends": "../../tsconfig.base.json", "compilerOptions": { "composite": true, "rootDir": "src", "paths": { "@/*": ["./src/*"] } }, "references": [{ "path": "../../packages/ui" }], "include": ["src"] }

Rules I keep:

  • paths are for the package that owns them, mirrored in the bundler alias. A paths entry with no Vite/Webpack equivalent is editor cosplay.
  • Prefer project references over one giant root include once packages multiply. One root that typechecks the universe makes every edit wait on the world.
  • skipLibCheck: true remains the default for app repos, checking all of node_modules types is rarely the bug you meant to catch. Library authors publishing types should still care about their own emit.

Failure modes

Strict in base, loose in app overrides. A package extends the strict base then sets "strict": false "temporarily." Temporary becomes culture. If an app needs a narrower exclude list, exclude files, do not disable strict.

// @ts-expect-error without a ticket. Expect-error is a scalpel. Require a comment that names the bug or the upstream type gap. Bare suppressions are how maps of shame form.

Different jsx / moduleResolution per app for no reason. Copy-paste configs drift. Extend a base; override only what the runtime requires (jsx for React, nodenext for a CLI package).

Believing incremental fixes architecture. Incremental helps; it does not replace splitting projects or deleting dead include globs.

A practical rollout order

  1. strict: true green on the pilot package.
  2. Turn on noUncheckedIndexedAccess for domain/shared packages; fix call sites.
  3. Evaluate exactOptionalPropertyTypes on API types before UI props.
  4. Align Vite aliases with paths; add a CI step that runs tsc -b (or per-package tsc --noEmit) on every PR.
  5. Only then argue about niche flags.

Takeaway

strict is the floor. The extras, especially noUncheckedIndexedAccess, are where production data shapes stop lying. The bundler will not save you; wire tsc as a CI gate and treat editor/CI skew as a project-graph bug, not a type theory debate.

Next arc shift: SPA vs MPA trade-offs, navigation, SEO, data, and team shape - before we pick Vite as a concrete SPA baseline.