Command Palette

Search for a command to run...

Hectal
PHASE 12Intermediate ~24 min· topic 2 of 6

Topic 12.2

The strict Family and Extra Checks

In one line

strict is a bundle of nine switches, each of which closes one specific hole in the type system, and a few extra flags go beyond it. Knowing the real bug each one catches tells you what to keep on and how to turn them on one at a time in an old codebase.

Think of it like this

A building inspector checks a house in separate steps: wiring, gas pipes, fire exits, stair rails. You can pass the wiring check and still fail on the stair rails. Each strict flag is one inspection. strict: true means "run all the standard inspections", and the extra flags are optional ones a careful owner adds.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

strict
A switch that turns on a family of safety flags at once. On by default since TypeScript 6.0.
Implicit any
A value that got the type any only because nothing said otherwise. noImplicitAny makes this an error.
strictNullChecks
Makes null and undefined their own types, so a possibly-missing value must be checked before use.
Index access
Reading by position or key: items[3], record[key]. noUncheckedIndexedAccess adds | undefined to the result.
Optional property
A property written with ?, like nickname?: string. It may be missing.
Fallthrough
In a switch, running on into the next case because a case did not end with break, return or throw.
Style flag
An extra check about discipline rather than type safety, such as noUnusedLocals. Off unless you turn it on.
@ts-expect-error
A comment that says the next line has an error on purpose. If the line stops erroring, the comment itself becomes an error.

Step by step

01One flag, one hole: undefined sneaks in

A User has an optional nickname. Without strictNullChecks, TypeScript treats nickname as a plain string, so user.nickname.length looks fine. At runtime it is undefined and the program throws.

With the flag on, both uses are errors in the editor. Each error is TypeScript pointing at a real crash before any user can see it. The two live examples below run the same code under the two settings.

02Reading by index might find nothing

planets[3] on a three-item array gives undefined at runtime. Without noUncheckedIndexedAccess the type is string, a small lie. With it, the type is string | undefined and you must handle the missing case: planets[3]?.toUpperCase() or an if.

The cost is real in loops (for (let i = 0; ...) arr[i] is T | undefined). Prefer for...of, .map, destructuring with a default, or .at() plus a check, which don't need the extra flag to be safe.

03Turn strictness on one flag at a time

Here is a small old project with strict: false. cart.ts has an untyped total(items) parameter and main.ts uses the result of find without checking it.

First noImplicitAny alone: one error. Then add strictNullChecks: one more. Each step is small enough to fix in one commit.

terminalwhole filebash
# tsconfig: "strict": false
$ tsc
(no output, exit 0)

# add "noImplicitAny": true
$ tsc
src/cart.ts(1,23): error TS7006: Parameter 'items' implicitly has an 'any' type.

# add "strictNullChecks": true
$ tsc
src/cart.ts(1,23): error TS7006: Parameter 'items' implicitly has an 'any' type.
src/main.ts(5,13): error TS2532: Object is possibly 'undefined'.

04Order of the flags in a migration

noImplicitAny first: it forces types onto the code and makes every later check more precise. strictNullChecks second: usually the biggest, so do it folder by folder. Then strictFunctionTypes, strictBindCallApply and strictPropertyInitialization, which are small. Finally replace the list by "strict": true.

Keep each step in its own pull request. If one step produces too many errors, add // @ts-expect-error TODO comments, merge, and burn them down over time. They are searchable and will fail the build the day they become unnecessary.

Order of the flags in a migrationdiagram
Rendering diagram…

05The one that needs a partner

exactOptionalPropertyTypes only makes sense with strictNullChecks. If a config turns strict off and keeps the first one on, the compiler stops with an error in the config itself, before it checks any code.

terminalwhole filebash
# tsconfig: { "strict": false, "exactOptionalPropertyTypes": true }
$ tsc
tsconfig.json(4,5): error TS5052: Option 'exactOptionalPropertyTypes' cannot be specified without specifying option 'strictNullChecks'.

Try it yourself

  1. 1

    Fix the null bugs

    In the third example, change line 6 to const n: string = user.nickname ?? user.name; and line 8 to return u.nickname?.length ?? 0;. Predict the Types panel and the console, then check: no errors, and the output is Asha and 0.

  2. 2

    Handle the missing planet

    In the noUncheckedIndexedAccess example, replace the last line with console.log(fourth?.toUpperCase() ?? "no such planet");. The error disappears and the program prints a message instead of crashing.

  3. 3

    Stop the fallthrough

    In the switch example, add break; after name = "Sunday";. The Types error goes away and the output becomes Sunday weekend weekday. Now remove that break; again and put a new line case 5: directly above case 6:. Is that a second error? No: an empty case that stacks onto the next one is not flagged. The flag complains only about a case that has statements (like name = "Sunday";) and then runs on, which is why case 0: stays the only error.

  4. 4

    Allow undefined on purpose

    In the exactOptionalPropertyTypes example, change the property to theme?: "light" | "dark" | undefined;. Line 5 becomes valid, because the type now says undefined is a value you accept.

Code & diagrams

The strict family: five bugs, five different flags

Five errors, each from a different flag: noImplicitAny (line 1), strictPropertyInitialization (line 7), strictFunctionTypes (line 17), strictBindCallApply (line 22) and useUnknownInCatchVariables (line 27). The program still starts, and it crashes on the first bug TypeScript predicted: a function that wanted text received 42.

Console

(nothing was printed)

Expected console output

Uncaught TypeError: e.toUpperCase is not a function
The same bugs with strictNullChecks off: no warning, same crash

exactOptionalPropertyTypes has to be switched off together with strictNullChecks (otherwise TS5052). The Types panel is empty, yet the last line throws. Remove the tsconfig override in your head: the next example is this code with the checks on.

Console

(nothing was printed)

Expected console output

undefined
Uncaught TypeError: Cannot read properties of undefined (reading 'length')
strictNullChecks on: both uses are errors

Line 6 assigns string | undefined to a string. Line 8 reads .length on something that may be missing. The runtime result is identical to the previous example: the error is the warning, not a cure. Fix it with `user.nickname ?? user.name` and `u.nickname?.length ?? 0`.

Console

(nothing was printed)

Expected console output

undefined
Uncaught TypeError: Cannot read properties of undefined (reading 'length')
noImplicitReturns: a path that forgets to return

grade has an if with no else: when the score is low the function falls off the end and returns undefined without saying so. With noImplicitReturns the compiler asks you to write return undefined; or restructure. The Types panel is empty with the option off.

Console

(nothing was printed)

Expected console output

pass undefined fail
noFallthroughCasesInSwitch: the missing break

describe(0) should say Sunday, but case 0 has no break, so it runs on into case 6 and overwrites name. The output shows the bug (weekend weekend weekday); the flag flags the line that causes it. Empty cases that stack (case 1: case 2:) are allowed.

Console

(nothing was printed)

Expected console output

weekend weekend weekday
noUncheckedIndexedAccess: reading past the end

The ^? answer is string | undefined, not string: the checker can't know how long an array is. Try planets[0] and see that it is also string | undefined. Write planets[3]?.toUpperCase() or check first. Everyone who reads an array by number meets this flag in week one.

Console

(nothing was printed)

Expected console output

Uncaught TypeError: Cannot read properties of undefined (reading 'toUpperCase')
exactOptionalPropertyTypes: missing is not the same as undefined

theme? means the key may be absent. Writing theme: undefined puts the key there with the value undefined, and "theme" in b is true. The flag makes the two states different on purpose. To allow both, write theme?: "light" | "dark" | undefined.

Console

(nothing was printed)

Expected console output

{ theme: 'dark' } { theme: undefined } true
The style flags: override, unused names, index signatures

Four style flags are on at once and each finds one thing. With no override in the tsconfig, this example type-checks clean and prints the same two lines. A leading underscore (_unused) tells noUnusedParameters you meant it.

Console

(nothing was printed)

Expected console output

hectal.in
Woof 100

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Read past the end of an array

Keep noUncheckedIndexedAccess on and call a method on planets[3] without checking it.

terminal
$ Types panel
── what you'll see ──
index.ts(4,13): error TS18048: 'fourth' is possibly 'undefined'.

Break #2

Turn off strict but keep the exact flag

Set "strict": false and leave "exactOptionalPropertyTypes": true in the tsconfig.

terminal
$ tsc
── what you'll see ──
tsconfig.json(4,5): error TS5052: Option 'exactOptionalPropertyTypes' cannot be specified without specifying option 'strictNullChecks'.

Myth vs fact

Myth

strict: true turns on every check TypeScript has.

Fact

It turns on the nine strict-family flags only. noUncheckedIndexedAccess, exactOptionalPropertyTypes, noImplicitReturns, noFallthroughCasesInSwitch, noImplicitOverride, the unused-name flags and noPropertyAccessFromIndexSignature are all separate.

Myth

Turning a strict flag on changes the JavaScript that is produced.

Fact

Flags change what is reported, not what is emitted. The one exception is alwaysStrict, which adds "use strict" to the output.

Myth

Use any or as to get through a strict migration quickly.

Fact

That deletes the check you are trying to adopt. Use // @ts-expect-error with a note: it is honest, searchable, and fails the build when it becomes unnecessary.

Myth

noUncheckedIndexedAccess makes for...of and map annoying too.

Fact

It only affects reads by index or key. Iterating gives you the item type directly, with no | undefined.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    strictNullChecks is what makes narrowing (Topic 4.3) worth having: with it off, string and undefined are not distinct, so there is nothing to narrow. Most advanced typing in later phases assumes it is on.

  • ▸

    strictFunctionTypes applies only to function-typed properties and parameters, not to method-style declarations. interface Box { set(v: string): void } (method syntax) is still checked bivariantly, which is why Array<T> is covariant in practice. This is one of the deliberate unsound spots discussed in Topic 16.5.

  • ▸

    exactOptionalPropertyTypes also applies to Partial<T> and to spread results: const p: Partial<{ theme: "a" | "b" }> = { theme: undefined } is TS2375 too, because Partial only adds ?. If a library ships .d.ts files written without it, skipLibCheck hides the mismatch inside the library, but your own calls into it are still checked: expect to add | undefined to some of your own optional types.

  • ▸

    noUnusedLocals and noUnusedParameters are better as lint rules in many teams (they are autofixable and editor-visible while typing). noImplicitOverride is safer in the compiler, because renaming a base method otherwise silently breaks every subclass.

Remember this

  1. 1

    The problem this solves: JavaScript lets many bugs through (a missing value used as if it were there, a forgotten return, a wrong argument), and TypeScript's checks for them are switches you can turn on or off. Knowing what each switch catches lets you choose how strict your project is. strict: true turns on a family of flags together: noImplicitAny, strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, strictBuiltinIteratorReturn, noImplicitThis, useUnknownInCatchVariables and alwaysStrict. You can switch one of them off while the rest stay on ("strict": true, "strictNullChecks": false), and a flag you write always beats what strict implies. The one exception is alwaysStrict: TypeScript 6.0 deprecates turning it off and 7.0 rejects "alwaysStrict": false with TS5108. In TypeScript 6.0 and 7.0 strict is on unless you say otherwise.

  2. 2

    Each flag fixes one hole. **noImplicitAny**: a parameter or variable with no annotation and nothing to infer from used to become any silently; now it is an error (TS7006). **strictNullChecks**: null and undefined become their own types, so a value that might be missing can't be used as if it were there (TS18048, TS2322). It is the single most valuable flag; without it, every type quietly includes undefined. **strictFunctionTypes: a function that accepts less can't stand in where a function that must accept more is expected (Topic 7.7 teaches this, and Topic 16.5 returns to it). strictBindCallApply**: f.call(null, 1, "2") is checked against the real parameter types of f.

  3. 3

    **strictPropertyInitialization**: a class property declared as label: string must be assigned in the constructor or have an initializer, or you get TS2564. **noImplicitThis**: using this in a function where its type can't be known is an error. **useUnknownInCatchVariables** (TypeScript 4.4): the variable in catch (err) is unknown, not any, because anything can be thrown; you must check before using it (Topic 13.3). **strictBuiltinIteratorReturn**: built-in iterators such as map.entries() end with undefined as their return value rather than any. **alwaysStrict**: emit "use strict" in every file, which is JavaScript's own strict mode.

  4. 4

    Beyond strict, tsc --init turns on two extra checks in the generated file. **noUncheckedIndexedAccess** adds | undefined to anything read by an index: planets[3] is string | undefined, because the array might be shorter than you think, and so is record[key]. It makes loops and lookups a little more verbose and catches real crashes. **exactOptionalPropertyTypes** (4.4) makes nickname?: string mean "may be missing" and not "may be present as undefined". Writing nickname: undefined becomes an error, which matters when code later asks "nickname" in obj or when an API treats "missing" and "undefined" differently. It needs strictNullChecks; turning strict off and leaving it on gives TS5052.

  5. 5

    Then come the style flags, commented out in the --init file because they are about taste and CI discipline, not safety of the type system. **noImplicitReturns**: every path of a function must return a value (TS7030); **noFallthroughCasesInSwitch**: a non-empty case must end with break, return or throw (TS7029), so a missing break can't silently run the next case. **noImplicitOverride**: a method that overrides a parent method must say override (Topic 6.3). **noUnusedLocals and noUnusedParameters**: unused names are errors (TS6133). **noPropertyAccessFromIndexSignature**: headers.host on a Record<string, string> must be written headers["host"] (TS4111, Topic 3.6), so you can see at a glance which names are guaranteed fields and which are lookups.

  6. 6

    Adopting strictness in an old project is done flag by flag, not by flipping strict and facing ten thousand errors. Start with strict: false and noEmit, then turn on noImplicitAny and fix, then strictNullChecks and fix, then add the rest and finally replace the list with strict: true. Two escape hatches help while you migrate: // @ts-expect-error above a line (it is itself an error once the line stops failing, so it cleans itself up) and giving a hard file a looser config. Don't use any or as to silence errors you haven't understood: you are deleting the check you just paid for.

  7. 7

    A flag changes what the checker reports, never what the JavaScript does. Most of the live examples below print the same output with a flag on or off; the difference is that TypeScript warned you about the bug that the output shows. Pick the strictest set your team can keep green, and enforce it with tsc --noEmit in CI (Topic 12.5).

Explain it without notes

01

What does strict include, and what does it leave out?

02

Why is strictNullChecks called the most important strict flag?

03

What does noUncheckedIndexedAccess change, and what does it cost?

04

What is the difference between x?: string with and without exactOptionalPropertyTypes?

05

How would you move a large codebase from strict: false to strict: true?

Practice

01

This function compiles with strict: false but crashes sometimes. Turn on the right flag and fix it: ``ts function firstWord(text: string | null) { return text.split(" ")[0]; }

02

Predict which flags catch each of these lines: const first = names[0].trim(); where names: string[]; function f(x) {}; a switch case that assigns and then has no break before the next case.

Trade-offs

  • ↔

    Stricter flags catch more bugs but ask for more code to handle the missing and unexpected cases. For a prototype, a looser set is faster; for code a team keeps for years, strict plus noUncheckedIndexedAccess pays for itself quickly.

  • ↔

    Style flags in the compiler fail the build on things a linter can warn about. Teams often keep safety flags in tsc and unused-name rules in the linter, so a half-finished edit doesn't block the dev server.

Done when you can

  • I can name the flags that strict turns on and give a one-line bug for each.

  • I can explain what noUncheckedIndexedAccess and exactOptionalPropertyTypes change.

  • I can name the style flags and say which are not part of strict.

  • I can plan a gradual migration from strict: false with flags in a sensible order.

  • I can use // @ts-expect-error instead of any during a migration.

  • I know that flags change what is reported, not the JavaScript that runs.