/ 3 min read

TypeScript Strict Mode as Your First Defense


This is Part 4 of a series on plan-based development with Claude Code. Today we explore why TypeScript strict mode accelerates rather than slows development.


Beyond “Any” - Strict Mode as a Feature


Many teams adopt TypeScript but undermine its value with loose configurations. They use any liberally, disable strict checks, and treat types as documentation rather than guarantees.


We took the opposite approach: TypeScript strict mode everywhere, with additional constraints.


Our base tsconfig.json:


{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"exactOptionalPropertyTypes": true,
"forceConsistentCasingInFileNames": true
}
}

Why Strict Mode Accelerates Development


Counter-intuitively, stricter types make you faster:


1. Errors Surface at Write Time, Not Run Time


// Without strict mode, this compiles fine and crashes at runtime
function getUser(id: string) {
const users = { alice: { name: "Alice" } };
return users[id].name; // Runtime error if id !== "alice"
}
// With noUncheckedIndexedAccess, TypeScript catches this
function getUser(id: string) {
const users = { alice: { name: "Alice" } };
const user = users[id]; // Type: { name: string } | undefined
return user?.name ?? "Unknown"; // Must handle undefined
}

2. Refactoring Becomes Fearless


When you rename a property or change a function signature, TypeScript shows you every call site that needs updating. In a 60+ package monorepo, this is invaluable.


3. Self-Documenting Code


With precise types, you often don’t need comments:


// Types tell the story
type ProjectStatus = "draft" | "active" | "completed" | "archived";
interface CreateProjectInput {
name: string;
clientId: string;
startDate: Date;
status?: ProjectStatus; // Defaults to "draft"
}
function createProject(input: CreateProjectInput): Promise<Project> {
// Implementation
}

Our Type Safety Rules


We’ve codified our type safety practices:


## Type Safety
- Never use `as unknown as X` without justification
- Use type guards or Zod validation instead of casts
- Validate JSONB at boundaries with Zod schemas
- Use shared types for `Result`, `StrictOmit`, `DeepPartial`

The Post-Change Verification Ritual


After any significant change, we run:


Terminal window
bun run check && bun run check-types

This becomes muscle memory. The commands:

  • check: Runs oxlint for fast linting
  • check-types: Runs TypeScript compiler across all packages

Types as Architecture Documentation


Our package structure uses types to enforce boundaries:


packages/api-base/src/procedures.ts
export const protectedProcedure = baseProcedure.use(async ({ context, next }) => {
if (!context.session) {
throw new ORPCError("UNAUTHORIZED");
}
return next({
context: {
...context,
session: context.session, // Now typed as non-null
},
});
});

Anyone using protectedProcedure gets session guaranteed in context. The type system enforces what comments could only suggest.


The Compound Effect


Strict typing pays dividends that compound over time:


  • Day 1: Extra time adding types feels slow
  • Week 1: Autocomplete and error detection speed you up
  • Month 1: Refactoring confidence lets you improve code fearlessly
  • Month 6: New team members onboard faster, understanding code through types
  • Year 1: Technical debt is lower because types prevented shortcuts

Next up in Part 5: Testing as a feedback loop - how we prioritize fast tests over comprehensive coverage.