TypeScript from the ground up

The compiler and its settings

tsconfig, strict mode and what it turns on, the difference between `any` and `unknown`, and where the type checker sits in a real build.

Part 12 of 13

Advanced15 min readBack to Courses

The file that decides how strict the check is

A TypeScript project is governed by tsconfig.json. The one setting that matters most is "strict": true, which turns on a family of checks at once — the largest being strictNullChecks, the rule that makes null and undefined impossible to ignore.

Without it, every value quietly accepts null, and the crash is a run-time surprise. With it, the optional value is a union you have to handle — which is the same narrowing you met in the unions chapter, applied to the whole program. The setting is per project rather than per file, so a new file inherits the same rules without anyone remembering to opt in.

A strict configuration, in full

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

any and unknown are not the same permission

any switches checking off: the value can be used as anything, and a mistake flows on unchecked. unknown is the safe sibling — anything may be stored in it, but nothing may be done with it until a check has narrowed it.

That is why unknown is the right type at the borders of a program: a JSON response, a caught error, a message from outside. Let the unknown in, then narrow it with the same tools you use everywhere else.

Where the checker sits

Types are erased before the code runs, so the checker itself is not shipped. In a modern setup the checker and the bundler are two different tools: the bundler strips the types to build, and the checker runs beside it to report mistakes — in an editor, or in CI as a gate.

The compiler itself changed recently: TypeScript 7 is the same language with the compiler rewritten in Go, an order of magnitude faster on large projects. Nothing in this course changes with it — the types are the language, and the tool around them keeps getting quicker.

What strict actually turns on

strict is a bundle, not one switch. The best-known member is strictNullChecks, which stops null and undefined from being assignable to every type. noImplicitAny refuses a parameter the compiler had to guess, and strictFunctionTypes checks callback parameters more carefully.

Turning the bundle on is the start, not the end. noUncheckedIndexedAccess is not part of strict and is worth adding: it makes list[0] a T | undefined, because the index may be past the end.

Where the types come from

The DOM types, console and JSON are declared in .d.ts files that ship with TypeScript. A library without bundled declarations needs @types/package, which is only a description — it adds no code to the bundle.

That is why a missing @types package produces errors about names you can see working in the browser. The runtime has the function; the checker has not been told its shape.

How the checker runs beside the build

A bundler such as esbuild or Vite does not type-check. It strips the annotations and emits JavaScript, because that is what makes it fast. The checker is a separate process: tsc --noEmit reads the same files, builds the type model, and writes nothing — its only output is the list of errors.

That split is why a build can succeed and CI can fail, or the reverse. The bundler only asks whether the syntax is valid; the checker asks whether the types agree. A project usually runs both: the bundler for the artefact, tsc --noEmit for the gate.

What goes wrong

The classic strict-mode error is Object is possibly ’null’ on a value that cannot be null. It usually means the code is relying on an invariant that lives in someone’s head — the element exists, the array has items, the response has a body. The fix is to write the invariant down as a check, not to widen the type.

The second is a // @ts-expect-error that stays after the error is fixed. It is the one suppression the compiler polices: when the next line stops being an error, the comment itself becomes one. @ts-ignore does not do that, which is why it accumulates.

Three habits that undo strict mode. // @ts-ignore silences the line and keeps the bug. any at a boundary does the same more politely. And a ! after a possibly missing value asserts a fact you have not checked — write the check instead, and the compiler will narrow for the rest of the block.

Try these next

  • Add noUncheckedIndexedAccess to a tsconfig and find the list reads that now need a check.
  • Replace one any with unknown and narrow it at the point of use.
  • Write a .d.ts-style declaration for a small function and see where the editor picks it up.

Nothing known yet

Replace the unchecked parameter with `unknown`, then narrow it so text and lists are both handled and anything else is refused.

Live Preview
Console output will appear here...

3 left to do

  • The parameter accepts anything, unchecked-free
  • The text case is separated
  • The list case is separated

After typeof value === "string" the value is text; a list is separated with Array.isArray(value). Everything that is neither falls through to an error.

A number from the outside

Take an `unknown` value, accept a real number or a numeric string, and fall back to zero for everything else.

Live Preview
Console output will appear here...

3 left to do

  • The parameter is unknown, not any
  • The number case is checked
  • The converted value is checked for usability

typeof value === "number" handles the number case; after a string check, Number(value) converts the text. Number.isFinite rejects the result when it is not a usable number.

Turn strict on before a project has code, not after. Switching it on later turns every unchecked null into an error list — true, useful, and much harder to read all at once.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What is the largest thing "strict": true turns on?