tsconfig & Strict Mode

The configuration file that controls how the TypeScript compiler behaves, and the single setting that turns on its strongest safety checks.

What is it?

Every setting you've relied on so far — which files to check, which JavaScript version to compile down to, how strict the checking should be — has to be configured somewhere. That somewhere is a file called tsconfig.json, placed at the root of a TypeScript project. It tells the compiler (and your editor) which files belong to the project, where to put the compiled output, and dozens of individual options controlling exactly how picky the type checker should be.

Among those dozens of options, one deserves special attention: strict. Setting "strict": true doesn't add one check — it's a single switch that turns on a whole bundle of stricter individual settings at once (things like requiring every variable's type to be known rather than silently falling back to any, and requiring you to explicitly handle the possibility that a value might be null or undefined). Most new TypeScript projects enable it from day one, because retrofitting strictness onto a large, already-loose codebase later is far more painful than starting strict.

Explain like I'm 10

tsconfig.json is like the rulebook for a referee before a match starts — it decides which parts of the field are in play and how strictly fouls get called. strict: true is like telling that referee "call every single foul, no exceptions" instead of only stepping in for the obvious ones — it catches far more, but it also means the game gets paused more often until everyone's actually playing by the rules.

Examples

A minimal tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "strict": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"]
}

target sets which JavaScript version the output is compiled to. module sets the module system used in the output. outDir/rootDir control where compiled files go. include tells the compiler which files are part of this project. strict turns on the full bundle of stricter checks.

What strict mode actually catches

// with "strict": false (or strictNullChecks off)
function getLength(text: string) {
  return text.length;
}
let input: string = null; // allowed without strict — a silent trap
getLength(input);          // crashes at runtime: "Cannot read properties of null"

// with "strict": true
let strictInput: string = null;
// Error: Type 'null' is not assignable to type 'string'.
// You are now forced to handle it explicitly:
let safeInput: string | null = null;
if (safeInput !== null) {
  getLength(safeInput); // only reachable once null has been ruled out
}

Without strict mode's strictNullChecks, null can silently masquerade as a string, leading to a runtime crash. With strict mode, TypeScript forces you to acknowledge and handle the possibility of null before using the value, at compile time instead of at a crash site in production.

How it works

When you run the TypeScript compiler (or start your editor's TypeScript integration), it looks for a tsconfig.json in the project and reads every option under compilerOptions to decide how to behave — which files to include, what JavaScript features to allow or compile away, and which categories of type errors to report. "strict": true is implemented as a shorthand that turns on a specific list of individual flags together, including noImplicitAny (errors on variables the compiler can't infer a type for, instead of quietly treating them as any), strictNullChecks (treats null and undefined as distinct from every other type, rather than assignable to anything), and several others. Each of those flags can still be set individually if you want strictness in some areas but not others, but strict is the common, all-at-once starting point.

tsconfig.json
     ↓
compilerOptions read by tsc / editor
     ↓
strict: true expands into:
  - noImplicitAny
  - strictNullChecks
  - strictFunctionTypes
  - strictBindCallApply
  - strictPropertyInitialization
  - noImplicitThis
  - alwaysStrict
  - useUnknownInCatchVariables
     ↓
every one of those checks applied while compiling

Why does it exist?

Without a shared configuration file, every developer and every editor touching a project could apply different rules about what counts as a type error, making the project's guarantees inconsistent from machine to machine. tsconfig.json centralizes that decision once, for the whole project. strict exists on top of that because TypeScript's individual strictness flags were added gradually over time, for backward compatibility — bundling them under one flag gives new projects an easy, well-tested way to opt into the full, intended level of safety at once, rather than having to discover and enable each flag separately.

When to use it

Enable "strict": true on essentially every new TypeScript project — the extra rigor pays for itself many times over by catching real bugs (like unhandled null values) at compile time. Reach into individual flags inside compilerOptions (like customizing target for the environments you support, or paths for import aliases) whenever a project's specific needs call for it.

When not to use it

Turning on strict partway through a large, long-running, loosely-typed codebase all at once will likely surface a large number of pre-existing errors simultaneously, which can be overwhelming. In that situation it's often more practical to enable the individual strict flags one at a time, fixing each category of error before moving to the next, rather than flipping the single switch and being buried in errors immediately.

Common mistakes

  • Assuming strict: true is just one check — it's a bundle of several distinct flags (noImplicitAny, strictNullChecks, and others), each catching a different category of mistake.

  • Starting a brand-new project with strict turned off "for now," intending to turn it on later — retrofitting strictness onto code already written loosely is far more work than starting strict.

  • Not realizing that changes to tsconfig.json may need the editor's TypeScript server restarted to take effect, leading to confusion about why new errors aren't showing up immediately.

Practice exercises

  1. Easy:

    Write a minimal tsconfig.json for a new project targeting ES2020, with strict enabled and source files under src/.

  2. Medium:

    Write a function that assigns null to a string-typed variable, and explain what error appears once strictNullChecks (part of strict) is enabled, and how to fix it properly.

  3. Hard:

    List three individual flags that strict: true turns on, and for each one, write a short code example of a mistake it would catch that plain (non-strict) TypeScript would allow through.

Interview questions

What is `tsconfig.json` for?

It's the configuration file, placed at a project's root, that controls how the TypeScript compiler behaves — which files to include, what JavaScript version to compile to, and which type-checking rules to enforce.

What does `"strict": true` actually do?

It's a shorthand that enables a whole bundle of individual stricter compiler flags at once — including noImplicitAny and strictNullChecks — rather than being a single check itself.

What does `strictNullChecks` specifically catch?

It stops null and undefined from being silently treated as assignable to every other type, forcing code to explicitly handle the possibility that a value might be missing before using it.

List the individual compiler flags that `strict: true` bundles together.

noImplicitAny, strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, alwaysStrict, and useUnknownInCatchVariables — enabling strict turns on all of these at once, and each can also be toggled individually.

What does `noImplicitAny` specifically do, separate from `strictNullChecks`?

It errors whenever the compiler can't infer a type for something (an untyped function parameter, for instance) and would otherwise silently fall back to any. It's about forcing every value to have some known type; it doesn't by itself change how null/undefined are treated — that's strictNullChecks's job.

What does `strictFunctionTypes` change about how function parameter types are checked?

It makes function parameter types checked contravariantly instead of bivariantly — a function expecting a narrower parameter type can no longer be substituted where one expecting a wider parameter type is required, since that substitution could let it be called with an argument it doesn't know how to handle. Without it, some unsound parameter substitutions were allowed for backward compatibility.

What does `strictPropertyInitialization` require of class fields, and how does it depend on `strictNullChecks`?

It requires every class property with a non-optional, non-undefined type to be definitely assigned by the end of the constructor (or have an initializer), since an uninitialized field would silently be undefined at runtime despite its declared type. It only takes effect together with strictNullChecks — without that flag, undefined is already assignable to everything, so there'd be nothing to flag.

What does `useUnknownInCatchVariables` change about `catch` blocks?

It types a caught error as unknown instead of any. Since JavaScript lets you throw any value, not just Error objects, typing it as unknown forces you to narrow it (e.g. check err instanceof Error) before accessing any property on it, rather than letting any silently allow anything through unchecked.

What does `alwaysStrict` do, and how is it different from the other strict-family flags?

It's not a type-checking flag at all — it emits JavaScript output with a leading "use strict"; and parses the input as strict-mode JS. The other flags under strict govern the type checker; alwaysStrict governs emitted code and parsing behavior, catching legacy sloppy-mode pitfalls like silent global variable creation rather than type mismatches.

What does `strictBindCallApply` add on top of the other strict flags?

It makes .bind(), .call(), and .apply() type-check the arguments passed against the original function's actual parameter types, instead of accepting anything. Without it, those three methods were typed loosely enough to let mismatched argument types through without an error.

A large, several-year-old codebase currently has `strict: false`. What's the recommended migration approach, rather than flipping `strict: true` directly?

Enable the individual flags one at a time — commonly starting with noImplicitAny, then strictNullChecks — fixing the errors each one surfaces before turning on the next, rather than flipping the single strict switch and being confronted with every category of error across the whole codebase at once.

Are `exactOptionalPropertyTypes` and `noUncheckedIndexedAccess` included when you turn on `strict: true`?

No — despite sounding like strictness flags, both are opt-in separately and are not part of the strict bundle. You have to enable each individually; enabling strict alone won't turn them on.

What does `noUncheckedIndexedAccess` do, and why isn't it part of the default `strict` bundle?

It changes indexing into an object via a string/number index signature to include | undefined in the result type, reflecting that the key might not actually exist at runtime. It's excluded from strict largely for adoption reasons — it can be very noisy on existing code that assumes every indexed key is always present, so it's left as an explicit, separate opt-in.

After enabling `strict: true` on an existing project, dozens of pre-existing null/undefined errors appear across files nobody's touching right now. What's a practical way to manage that without turning strict back off?

Suppress the specific pre-existing errors you're not ready to fix (e.g. a narrow // @ts-expect-error) while still catching new code that introduces the same class of bug — or migrate flag-by-flag instead of adopting the whole bundle in one step, so the error surface stays manageable.

Besides `compilerOptions`, what does the `include`/`exclude`/`files` section of `tsconfig.json` control?

Which files the compiler treats as part of the project at all — include (often a glob like "src/**/*") lists what should be checked, exclude removes matches from that set (commonly node_modules, build output), and files lists an explicit, exact set of entry files instead of a glob. None of this affects how strict the checking is — only which files get checked.

What's the difference between `target` and `lib` in `compilerOptions`?

target controls what JavaScript syntax version the compiler emits. lib controls which built-in type declarations are available to check against (whether Promise, newer Array methods, or DOM types like document are recognized) — the two are often set together, but changing one doesn't automatically change the other.

A Node.js backend project that never runs in a browser still has DOM types available by default, and code referencing `document` doesn't error as expected. What setting controls this?

The lib option — by default it's inferred from target and typically includes "dom". Explicitly setting lib to something like ["ES2020"] (omitting "dom") removes the ambient DOM globals, so referencing document or window would then correctly error.

What does `esModuleInterop` do, and what problem does it fix?

It changes how default imports from CommonJS modules are handled, letting import foo from "some-cjs-package" work correctly even when that package has no real ES-module default export — without it, you'd often need the more awkward import * as foo from "..." to interoperate with a CommonJS module using module.exports = ....

Why does TypeScript centralize configuration in a single `tsconfig.json` rather than letting each file specify its own compiler options?

Because type-checking guarantees are only meaningful if every file is held to the same rules — if one file could opt into looser checking than another, callers couldn't trust that a value typed as string in one file is really guaranteed to be a string once it flows into another. A single shared config keeps the project's safety guarantees consistent across every file and contributor.

What does `noEmitOnError` do, and why might a project want it enabled?

It stops the compiler from producing any JavaScript output at all if there are type errors, rather than emitting output anyway alongside the reported errors. Projects enable it to guarantee that code containing type errors can never accidentally get built and shipped, treating type errors as hard build failures instead of warnings.

Two developers run `tsc` on the same code with the same `strict: true` config and get different errors. What's a likely non-strict-related explanation?

They're likely running different TypeScript compiler versions — tsconfig.json controls options, not which version of tsc reads them, and newer compiler versions add checks or tighten inference in ways that can surface different errors on identical code and settings. Pinning TypeScript as a project dependency, rather than relying on a globally installed copy, avoids this.

You added a new strict flag to `tsconfig.json`, but your editor still isn't reporting the new errors it should. What's the likely fix?

The editor's TypeScript language server usually needs to be restarted (or the project reopened) to pick up a changed tsconfig.json — it doesn't necessarily re-read the config automatically on every save, so new compiler-option-driven errors can silently fail to appear in the editor until it's restarted, even though a fresh tsc run would catch them immediately.