skip to content

In TypeScript, what changes when the strictNullChecks compiler option is turned on, and what must code do differently before it can use a value that might be null or undefined?

level: juniorimportance: must knowfreq 78%

answer

  1. null and undefined stop being universal
  2. absence must live in the type
  3. narrow, chain, or default before use
  4. strict turns it on for you
  5. checking only — nothing is emitted

basics

~20 s

strictNullChecks gives null and undefined their own types instead of letting them inhabit every other type. A value typed string | undefined must then be narrowed with a check, optional chaining, or a default before it can be used as a string.

solid answer

~40 s

With `strictNullChecks` off, `null` and `undefined` are assignable to every type, so `const s: string = null` compiles and the type `string` tells you nothing about whether a value is actually there. Turning the flag on — it is included in `strict` — makes `null` and `undefined` distinct types that are only assignable where a union explicitly admits them, so absence has to be written into the type as `string | null` or `string | undefined`. Reading such a value as a `string` is then an error ("'x' is possibly 'undefined'") until control flow proves otherwise: an `if` check, an early return, `?.`, or a `??` default. Optional parameters and optional properties implicitly gain `| undefined`. The flag changes checking only — nothing is emitted, so no runtime check appears in the JavaScript.

go deeper

for a junior

Be able to say that null and undefined get their own types, so absence must be written as a union like string | null, and show one way to narrow it before use.

for a middle

Explain that the flag is part of strict, that optional parameters and properties implicitly gain | undefined, and how narrowing, ?. and ?? each satisfy the checker.

for a senior

Show where the guarantee stops: any, type assertions and unvalidated boundary data all defeat it, so demonstrate validating incoming payloads rather than asserting their shape.

for a principal

Own the rollout tradeoff — the error count when enabling it on a legacy codebase, whether to burn it down file by file, and what your published .d.ts files promise consumers about missing values.

## The two modes TypeScript's type layer is erased at compile time: the checker's whole job is to reject programs before they run. `strictNullChecks` decides how the checker models the two "no value" values that JavaScript has, `null` and `undefined`. With the flag **off** (the legacy default for a bare config), `null` and `undefined` belong to every type. `string` means "a string, or null, or undefined", and this compiles: ```ts const name: string = null; // no error without strictNullChecks name.toUpperCase(); // no error either — crashes at runtime ``` That is unsound by design: the annotation promises something the value does not deliver, and the failure surfaces as a `TypeError` in production instead of a red squiggle. With the flag **on**, `null` and `undefined` become ordinary types of their own. They are assignable only to themselves, to `any` and `unknown`, and — in `undefined`'s case — to `void`. The first line above becomes `Type 'null' is not assignable to type 'string'.` ## Writing absence into the type Once absence is no longer free, you say it explicitly: ```ts let middleName: string | undefined; function find(id: string): User | null { /* ... */ } ``` The union is now honest, and the checker holds you to it. Reading a member off `User | null` produces `'user' is possibly 'null'`, because the operation is only valid on one member of the union. ## Satisfying the checker Four everyday moves clear the error, and all of them are ordinary JavaScript the compiler happens to understand: ```ts const user = find(id); if (user !== null) user.email; // narrowing by a check if (!user) return; // early return, then user is User user?.email; // optional chaining → string | undefined const email = user?.email ?? ''; // nullish default → string ``` `?.` short-circuits to `undefined` when the base is `null` or `undefined`; `??` supplies the fallback for exactly those two values (and, unlike `||`, not for `0` or `''`). Both are runtime operators — they are not type-system features — but the checker tracks what they guarantee and narrows the type accordingly. ## Where undefined arrives implicitly Two pieces of everyday syntax add `| undefined` for you under this flag: ```ts function greet(title?: string) { /* title: string | undefined */ } interface Profile { bio?: string } // reading bio gives string | undefined ``` So a codebase that already used optional parameters and optional properties acquires a large number of new errors the day the flag is switched on. That is the flag working, not misbehaving: each one is a place where the code assumed a value that the signature never promised. ## What the flag does not do It does not emit anything. There is no generated null check, no runtime cost, and no reflection — the emitted JavaScript is identical either way. The guarantee is only as good as the types feeding it, which means three things still slip through: - **`any`** switches checking off for the value entirely, null included. - **A type assertion** (`value as string`) is a claim, not a conversion; the compiler performs no check and simply believes you. - **Untyped data at the boundary** — a JSON response typed as `User` because someone wrote `as User` — can contain `null` in any field, and the checker has no way to know. So `strictNullChecks` makes null-safety *provable inside* the typed part of your program, and the boundary where data enters is where you have to do real work: parse and validate, then hand the rest of the program a type that is actually true. ## Practical notes `strictNullChecks` is one of the flags `strict` turns on, so most modern projects have it without naming it. Enabling it on an existing codebase is usually done gradually, because the error count is proportional to how much the code relied on the loose model. And the flag is contagious in a good way: a library compiled with it exposes `.d.ts` files whose unions tell consumers where values can be missing, which is precisely the information the loose mode threw away.

  • If nothing is emitted, what stops a null from reaching code the compiler proved was null-free?
    Nothing, if bad types get in. The guarantee holds only over correctly typed values, so an `any`, a type assertion on a fetch response, or a mistyped third-party declaration can smuggle `null` past a proof. That is why validation belongs at the boundary: parse untrusted data into a type you have actually checked, rather than asserting a shape you hope is true.
  • How does an optional property differ from one you can simply set to undefined?
    Both read as `T | undefined`, so consumer code narrows them the same way. The difference is on the writing side: an optional property may be left off the object entirely, and by default TypeScript also permits explicitly assigning `undefined` to it. If a codebase needs "absent" and "present but undefined" to be different states, that distinction has to be made deliberately rather than assumed.
  • Why is `??` usually preferred over `||` for defaults once this flag is on?
    `||` falls back on every falsy value, so `0`, `''` and `false` are replaced by the default even though they are legitimate values. `??` falls back only on `null` and `undefined` — exactly the two cases the type says might be missing. Since strictNullChecks makes those two the ones you are actually defending against, `??` matches the type and `||` overshoots it.

saying these in an interview costs you the question

  • Claims TypeScript inserts runtime null checks
  • Says strictNullChecks must be enabled separately from strict
  • Uses a non-null assertion to silence every possibly-null error
  • Thinks an optional property is typed T rather than T | undefined
  • Assumes typed API responses cannot contain null

context