Skip to main content
</>Rosecraft Studios
typescriptweb-developmentbest-practices

TypeScript Strict Mode: Why We Never Ship Without It

5 min read

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 such
  • strictFunctionTypes — Function parameter types are checked contravariantly
  • strictBindCallApplybind, call, and apply methods are correctly typed
  • strictPropertyInitialization — Class properties must be initialized
  • noImplicitAny — No inferred any types
  • noImplicitThisthis must have an explicit type
  • alwaysStrict — 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
}
`Array.find()` returns `T | undefined`. Without strict null checks, TypeScript pretends the `undefined` case doesn't exist. This is the single most common source of runtime crashes in non-strict TypeScript codebases.

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
    }
  });
}
Think of `any` as "I'm lying to the compiler" and `unknown` as "I don't know yet, but I'll check before I use it." Prefer honesty.

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:

  1. Enable one flag at a time — Start with strictNullChecks, then noImplicitAny
  2. Fix errors file by file — Use // @ts-expect-error temporarily with a tracking comment
  3. Never add new any types — All new code must be properly typed
  4. Add noUncheckedIndexedAccess last — It generates the most changes
The TypeScript team estimates that `strictNullChecks` alone catches 70% of the bugs that strict mode prevents. If you enable only one flag, make it that one.

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.

Share this article

Corey Rosamond, Founder and Principal Engineer of Rosecraft Studios

Corey Rosamond

Founder & Principal Engineer

Learn more

Enjoyed this article?

Get notified when we publish new insights on web development and engineering.