Strict mode and key compiler checks

TypeScript · Practical · 12 min

The difference between leaky and trustworthy TypeScript is mostly configuration; these flags define what the checker catches.

"strict": true is a bundle of options, and new strict checks added in later versions join it automatically. The main ones:

  • noImplicitAny — unannotated parameters can't silently become any.
  • strictNullChecks — null/undefined are separate types.
  • strictFunctionTypes — function parameters are checked contravariantly (see the variance lesson).
  • strictPropertyInitialization — class fields must be initialised.
  • strictBindCallApply — call, apply and bind are checked against the function's signature.
  • useUnknownInCatchVariables — catch (e) gives e: unknown.
  • noImplicitThis and alwaysStrict.

You can switch individual members off ("strict": true, "strictPropertyInitialization": false) while migrating.

Valuable checks outside strict:

  • noUncheckedIndexedAccess — arr[i] and record[key] include undefined. Catches real out-of-range bugs; costs some ! or checks in loops (prefer for...of).
  • exactOptionalPropertyTypes — p?: string no longer accepts an explicit undefined.
  • noImplicitOverride — require override on overriding methods.
  • noImplicitReturns, noFallthroughCasesInSwitch — control-flow sanity.
  • noPropertyAccessFromIndexSignature — force obj["key"] for dynamic keys.

Project hygiene flags: isolatedModules (each file can be compiled alone, required by esbuild/swc/Babel), verbatimModuleSyntax, and skipLibCheck.

To migrate an existing codebase, enable strict, then suppress the remaining errors per line with // @ts-expect-error (which itself errors once the problem is fixed, unlike // @ts-ignore) and pay them down gradually. Tools like typescript-strict-plugin can enforce strictness file by file.

Example

// Compiled with: "strict": true, "noUncheckedIndexedAccess": true

const names = ["Ada", "Grace"];
const third = names[2];                  // string | undefined
// @ts-expect-error -- 'third' is possibly 'undefined'
console.log(third.toUpperCase());
for (const n of names) console.log(n.toUpperCase()); // for...of: n is string

const ports: Record<string, number> = { http: 80 };
const https = ports["https"];            // number | undefined
console.log(https ?? 443);

function greet(this: { name: string }, greeting: string) {
  return `${greeting}, ${this.name}`;
}
// strictBindCallApply checks arguments:
// @ts-expect-error -- Argument of type 'number' is not assignable to 'string'
greet.call({ name: "Ada" }, 42);
console.log(greet.call({ name: "Ada" }, "Hi"));

Remember

  • Always start with strict: true
  • Add noUncheckedIndexedAccess for honest index reads
  • Prefer @ts-expect-error over @ts-ignore for temporary suppressions
  • Use isolatedModules when another tool compiles your files

Watch out for

  • Disabling strict entirely to make a migration compile instead of disabling single sub-flags.

Check yourself

  1. Which flag makes arr[5] have type T | undefined?
  2. Why prefer // @ts-expect-error over // @ts-ignore?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.typescriptlang.org/tsconfig/#strict