TypeScript Strict Mode: Why We Never Ship Without It
The Case for Strict Mode
Every project at Rosecraft Studios starts with the same TypeScript configuration: strict: true. No exceptions, no negotiations. After 35 years of writing software, we've learned that the bugs you catch at compile time are infinitely cheaper than the ones you find in production at 2 AM.
TypeScript's strict mode isn't a single flag — it enables a family of compiler checks that, together, eliminate entire categories of runtime errors.
What Strict Mode Actually Enables
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
The strict flag is shorthand for:
strictNullChecks— Variables aren't nullable unless explicitly typed as suchstrictFunctionTypes— Function parameter types are checked contravariantlystrictBindCallApply—bind,call, andapplymethods are correctly typedstrictPropertyInitialization— Class properties must be initializednoImplicitAny— No inferredanytypesnoImplicitThis—thismust have an explicit typealwaysStrict— Emits"use strict"in every file
Real Bugs Caught by Strict Mode
1. The Null Reference That Wasn't
Without strictNullChecks, this code compiles fine and crashes at runtime:
// Without strict: compiles, crashes at runtime
function getUserName(users: User[], id: string) {
const user = users.find((u) => u.id === id);
return user.name; // Runtime error: Cannot read property 'name' of undefined
}
// With strict: compile error forces proper handling
function getUserName(users: User[], id: string) {
const user = users.find((u) => u.id === id);
if (!user) {
throw new Error(`User not found: ${id}`);
}
return user.name; // Safe — TypeScript knows user is not undefined
}
2. The Index Access Trap
Even with strictNullChecks, array and object index access is unsafe by default. That's why we add noUncheckedIndexedAccess:
const colors = ['red', 'green', 'blue'];
// Without noUncheckedIndexedAccess
const first = colors[0]; // Type: string (wrong — could be undefined)
// With noUncheckedIndexedAccess
const first = colors[0]; // Type: string | undefined (correct)
// Forces safe access patterns
if (first) {
console.error(first.toUpperCase()); // Safe
}
3. The Implicit Any That Hid a Bug
// Without noImplicitAny: compiles, returns wrong type
function parseConfig(raw) {
// 'raw' is implicitly 'any'
return raw.settings.theme; // No type checking at all
}
// With noImplicitAny: forces explicit typing
function parseConfig(raw: RawConfig): string {
return raw.settings.theme; // Fully type-checked
}
The unknown vs any Discipline
When a type is genuinely uncertain, reach for unknown instead of any. The difference is critical:
// 'any' disables ALL type checking
function processData(data: any) {
data.forEach((item) => item.transform()); // No errors, no safety
}
// 'unknown' requires narrowing before use
function processData(data: unknown) {
if (!Array.isArray(data)) {
throw new Error('Expected an array');
}
// TypeScript now knows data is an array
data.forEach((item: unknown) => {
if (isTransformable(item)) {
item.transform(); // Safe — type guard verified the shape
}
});
}
Zod: Runtime Validation Meets Static Types
At system boundaries — API routes, form handlers, external API responses — we use Zod schemas as the single source of truth for both validation and types:
import { z } from 'zod';
const contactSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
message: z.string().min(10).max(2000),
});
// Infer the TypeScript type from the Zod schema
type ContactForm = z.infer<typeof contactSchema>;
// API route uses the same schema for validation
export async function POST(request: Request) {
const body = await request.json();
const result = contactSchema.safeParse(body);
if (!result.success) {
return Response.json(
{ error: 'Validation failed', details: result.error.flatten() },
{ status: 400 },
);
}
// result.data is fully typed as ContactForm
await saveSubmission(result.data);
}
This pattern eliminates the type/validation drift problem where your TypeScript interface says one thing and your runtime validation checks something different.
Migrating to Strict Mode
If you have an existing codebase without strict mode, migrating all at once can be overwhelming. Our recommended approach:
- Enable one flag at a time — Start with
strictNullChecks, thennoImplicitAny - Fix errors file by file — Use
// @ts-expect-errortemporarily with a tracking comment - Never add new
anytypes — All new code must be properly typed - Add
noUncheckedIndexedAccesslast — It generates the most changes
The Bottom Line
Strict mode adds friction to development. You'll spend time satisfying the compiler that would otherwise be spent writing features. But every minute spent fixing a type error at compile time saves an hour of debugging the same error in production.
At Rosecraft Studios, strict TypeScript isn't a preference — it's a professional standard. Our clients' users never encounter a "Cannot read property of undefined" error because we caught it before the code was committed.
Building a TypeScript project and want it done right? Let's talk about how rigorous engineering practices translate to reliable software.
