Strict Mode Patterns
Recipe
Write React code that thrives under TypeScript's strictest settings. Master patterns for strictNullChecks, noUncheckedIndexedAccess, and exactOptionalPropertyTypes to catch bugs that looser settings miss.
Working Example
// strictNullChecks: handling nullable values safely
type User = {
id: string;
name: string;
address?: {
street: string;
city: string;
};
};
function UserAddress({ user }: { user: User }) {
// Without strictNullChecks, this silently returns undefined
// With strictNullChecks, TypeScript forces you to handle the case
if (!user.address) {
return <p>No address on file</p>;
}
return (
<p>
{user.address.street}, {user.address.city}
</p>
);
}// noUncheckedIndexedAccess: safe array and object access
const colors = ["red", "green", "blue"];
// Without noUncheckedIndexedAccess: colors[5] is string
// With noUncheckedIndexedAccess: colors[5] is string | undefined
function getColor(index: number): string {
const color = colors[index];
if (!color) {
return "default";
}
return color; // TypeScript now knows this is string, not string | undefined
}// Safe record access
type FeatureFlags = Record<string, boolean>;
function isFeatureEnabled(flags: FeatureFlags, key: string): boolean {
const value = flags[key];
// With noUncheckedIndexedAccess, value is boolean | undefined
return value ?? false;
}Deep Dive
How It Works
strict: trueenables a family of strict checks. The most impactful ones for React arestrictNullChecks,strictFunctionTypes, andnoImplicitAny.strictNullChecksmakesnullandundefineddistinct types that must be handled explicitly. A variable typed asstringcannot benullunless you type it asstring | null.noUncheckedIndexedAccessadds| undefinedto any index access on arrays and records. This catches the common bug of accessing an array index that might be out of bounds.exactOptionalPropertyTypesdistinguishes between a property that is missing and one that is explicitlyundefined.{ name?: string }means "name can be absent" but not{ name: undefined }.strictFunctionTypesenforces contravariant parameter checking on function types. This catches subtle bugs with event handler types and callback props.
Variations
Safe map/object lookups:
const userMap = new Map<string, User>();
function getUser(id: string): User {
const user = userMap.get(id); // User | undefined
if (!user) {
throw new Error(`User ${id} not found`);
}
return user; // Narrowed to User
}Non-null assertion as escape hatch:
// Use sparingly and only when you are certain the value exists
const canvas = document.getElementById("canvas") as HTMLCanvasElement;
const ctx = canvas.getContext("2d")!; // Non-null assertion
// Better: guard and throw
const ctx2 = canvas.getContext("2d");
if (!ctx2) throw new Error("Canvas 2D context not available");Handling optional chaining with strict nulls:
type Config = {
api?: {
baseUrl?: string;
timeout?: number;
};
};
function getApiUrl(config: Config): string {
return config.api?.baseUrl ?? "https://api.example.com";
}exactOptionalPropertyTypes in action:
// With exactOptionalPropertyTypes enabled:
type Settings = {
theme?: "light" | "dark";
};
// Allowed
const a: Settings = {};
const b: Settings = { theme: "light" };
// Error: undefined is not assignable to "light" | "dark"
// const c: Settings = { theme: undefined };TypeScript Notes
- Enable these incrementally if migrating: first
strict: true, thennoUncheckedIndexedAccess, thenexactOptionalPropertyTypes. - The
??(nullish coalescing) and?.(optional chaining) operators are your best tools under strict settings. They handlenull | undefinedcleanly. Array.prototype.find()returnsT | undefinedeven withoutnoUncheckedIndexedAccess. The setting mainly affects bracket accessarr[i].
Gotchas
noUncheckedIndexedAccessadds| undefinedto tuple access too. For tuples with known length, this can feel overly cautious. Use destructuring instead:const [first, second] = tuple.exactOptionalPropertyTypesbreaks code that explicitly passesundefinedto clear optional fields. You may need to usedelete obj.fieldinstead.- Non-null assertions (
!) bypass strict checks entirely. Every!is a potential runtime crash. Prefer guards and narrowing. - Third-party library types may not be compatible with your strict settings.
skipLibCheck: truehelps, but it hides issues in your own.d.tsfiles.
Alternatives
| Approach | Pros | Cons |
|---|---|---|
strict: true + noUncheckedIndexedAccess | Maximum safety, catches most bugs | More verbose null handling |
strict: true only | Good balance of safety and convenience | Misses array out-of-bounds bugs |
strict: false with incremental flags | Gradual migration path | Easy to miss important checks |
| Runtime validation (Zod) | Catches issues strict mode cannot | Added dependency, runtime overhead |
as assertions to bypass strictness | Quick fix for type errors | Defeats the purpose of strict mode |
FAQs
What checks does strict: true enable in TypeScript?
strictNullChecks,strictFunctionTypes,noImplicitAny,strictBindCallApply,strictPropertyInitialization, and others.- It is a family of flags, not a single check.
- The most impactful ones for React are
strictNullChecks,strictFunctionTypes, andnoImplicitAny.
How does strictNullChecks change the way you write React components?
- A variable typed as
stringcan no longer benullorundefinedunless explicitly typed asstring | null. - You must handle the nullable case with guards, optional chaining, or nullish coalescing.
- Without it, null/undefined access bugs are silent.
What does noUncheckedIndexedAccess do?
- Adds
| undefinedto any index access on arrays and records. colors[5]becomesstring | undefinedinstead ofstring.- Catches the common bug of accessing an out-of-bounds array index.
What is the difference between a missing property and one set to undefined under exactOptionalPropertyTypes?
type Settings = { theme?: "light" | "dark" };
const a: Settings = {}; // OK: property missing
const b: Settings = { theme: "light" }; // OK
// const c: Settings = { theme: undefined }; // ERROR- A missing property and
undefinedare treated as distinct. - You cannot explicitly assign
undefinedto an optional field.
Why is the non-null assertion operator (!) risky under strict mode?
- It bypasses strict null checks entirely, telling TypeScript "trust me, this is not null."
- Every
!is a potential runtime crash if the value actually is null or undefined. - Prefer guards, optional chaining (
?.), or nullish coalescing (??) instead.
Gotcha: Does noUncheckedIndexedAccess affect tuples?
- Yes. Even tuples with known length get
| undefinedon bracket access. - This can feel overly cautious for tuples.
- Use destructuring instead:
const [first, second] = tupleavoids the| undefined.
How should you handle safe array and record lookups under strict settings?
const colors = ["red", "green", "blue"];
function getColor(index: number): string {
const color = colors[index]; // string | undefined
if (!color) return "default";
return color; // narrowed to string
}- Always check for
undefinedafter index access. - Use
??for providing fallback values.
What is the recommended order for enabling strict flags incrementally?
- First:
strict: true(enables the core family of checks). - Second:
noUncheckedIndexedAccess(adds index safety). - Third:
exactOptionalPropertyTypes(strictest optional property handling).
Gotcha: Can third-party library types break under your strict settings?
- Yes. Libraries may not be written with
strictNullChecksorexactOptionalPropertyTypesin mind. skipLibCheck: truehelps by skipping type-checking of.d.tsfiles.- But this also hides errors in your own
.d.tsfiles.
What is the difference between ?. (optional chaining) and ?? (nullish coalescing)?
?.short-circuits toundefinedif the left side isnullorundefined:config.api?.baseUrl.??provides a fallback only fornullorundefined:value ?? "default".- Unlike
||,??does not trigger on falsy values like0or"".