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.
# 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.
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.
# 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
Fix the null bugs
In the third example, change line 6 to
const n: string = user.nickname ?? user.name;and line 8 toreturn u.nickname?.length ?? 0;. Predict the Types panel and the console, then check: no errors, and the output isAshaand0. - 2
Handle the missing planet
In the
noUncheckedIndexedAccessexample, replace the last line withconsole.log(fourth?.toUpperCase() ?? "no such planet");. The error disappears and the program prints a message instead of crashing. - 3
Stop the fallthrough
In the switch example, add
break;aftername = "Sunday";. The Types error goes away and the output becomesSunday weekend weekday. Now remove thatbreak;again and put a new linecase 5:directly abovecase 6:. Is that a second error? No: an emptycasethat stacks onto the next one is not flagged. The flag complains only about a case that has statements (likename = "Sunday";) and then runs on, which is whycase 0:stays the only error. - 4
Allow undefined on purpose
In the
exactOptionalPropertyTypesexample, change the property totheme?: "light" | "dark" | undefined;. Line 5 becomes valid, because the type now says undefined is a value you accept.
Code & diagrams
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 functionexactOptionalPropertyTypes 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')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')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 faildescribe(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 weekdayThe ^? 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')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 } trueFour 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 100Break 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.
Break #2
Turn off strict but keep the exact flag
Set "strict": false and leave "exactOptionalPropertyTypes": true in the tsconfig.
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.
- ▸
strictNullChecksis what makes narrowing (Topic 4.3) worth having: with it off,stringandundefinedare not distinct, so there is nothing to narrow. Most advanced typing in later phases assumes it is on. - ▸
strictFunctionTypesapplies 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 whyArray<T>is covariant in practice. This is one of the deliberate unsound spots discussed in Topic 16.5. - ▸
exactOptionalPropertyTypesalso applies toPartial<T>and to spread results:const p: Partial<{ theme: "a" | "b" }> = { theme: undefined }isTS2375too, becausePartialonly adds?. If a library ships.d.tsfiles written without it,skipLibCheckhides the mismatch inside the library, but your own calls into it are still checked: expect to add| undefinedto some of your own optional types. - ▸
noUnusedLocalsandnoUnusedParametersare better as lint rules in many teams (they are autofixable and editor-visible while typing).noImplicitOverrideis safer in the compiler, because renaming a base method otherwise silently breaks every subclass.
Remember this
- 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: trueturns on a family of flags together:noImplicitAny,strictNullChecks,strictFunctionTypes,strictBindCallApply,strictPropertyInitialization,strictBuiltinIteratorReturn,noImplicitThis,useUnknownInCatchVariablesandalwaysStrict. You can switch one of them off while the rest stay on ("strict": true, "strictNullChecks": false), and a flag you write always beats whatstrictimplies. The one exception isalwaysStrict: TypeScript 6.0 deprecates turning it off and 7.0 rejects"alwaysStrict": falsewithTS5108. In TypeScript 6.0 and 7.0strictis on unless you say otherwise. - 2
Each flag fixes one hole. **
noImplicitAny**: a parameter or variable with no annotation and nothing to infer from used to becomeanysilently; now it is an error (TS7006). **strictNullChecks**:nullandundefinedbecome 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 includesundefined. **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 off. - 3
**
strictPropertyInitialization**: a class property declared aslabel: stringmust be assigned in the constructor or have an initializer, or you getTS2564. **noImplicitThis**: usingthisin a function where its type can't be known is an error. **useUnknownInCatchVariables** (TypeScript 4.4): the variable incatch (err)isunknown, notany, because anything can be thrown; you must check before using it (Topic 13.3). **strictBuiltinIteratorReturn**: built-in iterators such asmap.entries()end withundefinedas their return value rather thanany. **alwaysStrict**: emit"use strict"in every file, which is JavaScript's own strict mode. - 4
Beyond
strict,tsc --initturns on two extra checks in the generated file. **noUncheckedIndexedAccess** adds| undefinedto anything read by an index:planets[3]isstring | undefined, because the array might be shorter than you think, and so isrecord[key]. It makes loops and lookups a little more verbose and catches real crashes. **exactOptionalPropertyTypes** (4.4) makesnickname?: stringmean "may be missing" and not "may be present asundefined". Writingnickname: undefinedbecomes an error, which matters when code later asks"nickname" in objor when an API treats "missing" and "undefined" differently. It needsstrictNullChecks; turningstrictoff and leaving it on givesTS5052. - 5
Then come the style flags, commented out in the
--initfile 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-emptycasemust end withbreak,returnorthrow(TS7029), so a missingbreakcan't silently run the next case. **noImplicitOverride**: a method that overrides a parent method must sayoverride(Topic 6.3). **noUnusedLocalsandnoUnusedParameters**: unused names are errors (TS6133). **noPropertyAccessFromIndexSignature**:headers.hoston aRecord<string, string>must be writtenheaders["host"](TS4111, Topic 3.6), so you can see at a glance which names are guaranteed fields and which are lookups. - 6
Adopting strictness in an old project is done flag by flag, not by flipping
strictand facing ten thousand errors. Start withstrict: falseandnoEmit, then turn onnoImplicitAnyand fix, thenstrictNullChecksand fix, then add the rest and finally replace the list withstrict: true. Two escape hatches help while you migrate:// @ts-expect-errorabove 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 useanyorasto silence errors you haven't understood: you are deleting the check you just paid for. - 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 --noEmitin CI (Topic 12.5).
Explain it without notes
What does strict include, and what does it leave out?
Why is strictNullChecks called the most important strict flag?
What does noUncheckedIndexedAccess change, and what does it cost?
What is the difference between x?: string with and without exactOptionalPropertyTypes?
How would you move a large codebase from strict: false to strict: true?
Practice
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];
}
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
noUncheckedIndexedAccesspays for itself quickly. - ↔
Style flags in the compiler fail the build on things a linter can warn about. Teams often keep safety flags in
tscand 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
strictturns on and give a one-line bug for each.I can explain what
noUncheckedIndexedAccessandexactOptionalPropertyTypeschange.I can name the style flags and say which are not part of
strict.I can plan a gradual migration from
strict: falsewith flags in a sensible order.I can use
// @ts-expect-errorinstead ofanyduring a migration.I know that flags change what is reported, not the JavaScript that runs.