TypeScript / Intermediate / Lesson 11
Strict flags and error boundaries
Choose compiler checks deliberately, interpret the uncertainty they expose, and handle thrown values without pretending every exception has an Error shape.
On this page
Lesson goals
- Distinguish strict-family checks from additional safety options.
- Explain exact optional writes and checked indexed reads.
- Narrow caught unknown values before inspecting them.
Strict is a policy family, not every possible check
strict enables a family of checks such as strictNullChecks, noImplicitAny, and strictFunctionTypes under the selected compiler version. Future compiler versions can add stricter behavior, so a pinned local compiler helps make diagnostics reproducible. noUncheckedIndexedAccess and exactOptionalPropertyTypes are additional explicit choices in this course. noEmitOnError controls output policy rather than strengthening an individual value type. noImplicitAny prevents some accidental any inference but does not ban explicit any or guarantee trusted external types. Record the effective configuration instead of assuming a flag name proves the whole application is safe.
Property presence can matter independently of its value
With exact optional properties, name?: string accepts omission but not an explicit undefined write unless undefined is included deliberately. Reading can still produce undefined. The first example intentionally permits both forms and observes them with Object.hasOwn. They have the same missing-like property value but different own-key presence, which can matter for patches, serialization, and defaults. The compiler does not choose the product meaning of that distinction. Define whether omission means unchanged and whether an explicit clear operation uses null, a separate command, or another documented representation.
export {};
type Options = { label?: string | undefined };
const omitted: Options = {};
const explicit: Options = { label: undefined };
console.log(Object.hasOwn(omitted, "label"), Object.hasOwn(explicit, "label"));
false true
Only the second object has its own label key. The union explicitly permits undefined in this contract.
Treat uncertainty as information to resolve
Checked indexed access exposes absent array or dictionary entries instead of pretending every lookup succeeds. strict property initialization catches some class fields that are not definitely assigned by construction, but a definite-assignment assertion is still only a checker promise. Resolve the real initialization or lookup condition rather than suppressing it. A framework may initialize a field later, in which case its lifecycle contract needs documentation and tests. Use a possibly undefined representation until the invariant is established when that is the honest model. Compiler certainty and runtime readiness must agree at the boundary where code relies on the value.
JavaScript can throw any value
JavaScript permits throwing strings, numbers, objects, and Error instances. With the course strict configuration, catch variables are treated as unknown unless otherwise constrained by an explicit annotation allowed there. The second example checks instanceof Error before reading message and uses a controlled fallback otherwise. instanceof has cross-realm and custom-prototype considerations, so an external structured error protocol may need shape validation rather than assuming one constructor identity. Do not cast every caught value to Error to silence message access. An error boundary should choose logging, propagation, or recovery without exposing secrets to users.
export {};
function message(error: unknown): string {
return error instanceof Error ? error.message : "unexpected failure";
}
try { throw new Error("bad input"); } catch (error) { console.log(message(error)); }
try { throw "raw failure"; } catch (error) { console.log(message(error)); }
bad input
unexpected failure
The Error supplies its controlled message. The string is not treated as an Error object and receives the fallback.
Roll stronger checks out with evidence
When enabling a new flag in an existing project, classify diagnostics by real uncertainty, inaccurate declarations, and incorrect implementation. Repair recurring patterns at their source instead of adding hundreds of assertions. Test the emitted program because a type repair can still change behavior. Keep temporary suppressions narrow, explained, and reviewed for removal; do not make any the default migration type. Compare effective settings across editor, build, tests, and continuous integration. A successful strict build is valuable evidence within its assumptions, while untyped dependencies, dynamic data, and deliberate unsound escape hatches still need runtime and boundary review.
Common errors and fixes
Strict is assumed to enable checked indexing automatically.
- Why it happens
- Additional options were not configured explicitly.
- How to fix it
- Enable and verify the intended indexed-access flag.
- Prevent it next time
- Inspect effective configuration in every build path.
A caught string is treated as Error.
- Why it happens
- An assertion replaced runtime narrowing.
- How to fix it
- Check the value or validate a structured error protocol.
- Prevent it next time
- Test non-Error thrown values.
A definite-assignment assertion hides an uninitialized field.
- Why it happens
- The checker promise does not match the lifecycle.
- How to fix it
- Initialize the field or model its not-ready state.
- Prevent it next time
- Test construction and early-access paths.
Check your understanding
1. Change Options to label?: string without undefined. Which first-example object still checks, and why does reading label remain possibly undefined?
Show worked solution
The omitted object still checks, while the explicit undefined assignment fails under exactOptionalPropertyTypes. Reading can yield undefined because the property may be absent. This separates write semantics from possible read results. If explicit undefined is a meaningful operation, include it deliberately rather than turning the flag off to avoid understanding the distinction.
2. An error logger receives null from a rejected promise. Describe a safe behavior and what a type annotation cannot guarantee.
Show worked solution
Treat the value as unknown, avoid reading message before checking, and record a controlled fallback or validated structured representation. A Promise type describes fulfillment but does not provide a typed rejection channel in ordinary JavaScript semantics. A declared Error expectation cannot force every library or caller to reject with Error. Test null and other primitive rejections explicitly.
Key takeaways
- Choose extra checks explicitly and pin their version.
- Resolve uncertainty with real conditions.
- Treat thrown values as unknown until justified.