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 runtimefunction getUser(id: string) { const users = { alice: { name: "Alice" } }; return users[id].name; // Runtime error if id !== "alice"}
// With noUncheckedIndexedAccess, TypeScript catches thisfunction 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 storytype 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:
bun run check && bun run check-typesThis becomes muscle memory. The commands:
check: Runs oxlint for fast lintingcheck-types: Runs TypeScript compiler across all packages
Types as Architecture Documentation
Our package structure uses types to enforce boundaries:
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.