Engineering
Overview
We turned on strict mode across a 60k-line codebase. Here's every error we hit, how we fixed them, and why we'd do it again.
Jordan Mercer
When we turned on strict: true across our 60,000-line codebase, TypeScript screamed at us with 847 errors. Three days later, we had a codebase that caught bugs before they reached production — and we'd never go back.
TypeScript compilation process
The strict flag enables five compiler flags that catch entire classes of bugs:
{
"compilerOptions": {
"strict": true,
// These all become true:
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true
}
}
// Before (Error: Parameter 'data' implicitly has an 'any' type)
function processData(data) {
return data.value
}
// After
interface DataPacket {
value: string
timestamp: Date
}
function processData(data: DataPacket) {
return data.value
}
Error visualization showing any types
// Before (Error: Object is possibly 'null')
const user = getUser()
console.log(user.name)
// After — Option 1: Guard clause
const user = getUser()
if (user) {
console.log(user.name)
}
// After — Option 2: Optional chaining
console.log(user?.name)
// After — Option 3: Nullish coalescing
console.log(user?.name ?? 'Anonymous')
// Before (Error: Property 'id' has no initializer)
class User {
id: number
name: string
}
// After — Definite assignment assertion
class User {
id!: number
name!: string
}
// Better — Constructor initialization
class User {
constructor(
public id: number,
public name: string
) {}
}
Start with the easiest fixes:
# Find files with most errors
npx typescript --noEmit --pretty false | grep "error TS" | cut -d'(' -f1 | sort | uniq -c | sort -rn | head -10
Create helper types for common patterns:
type Nullable<T> = T | null
type Optional<T> = T | undefined
type Maybe<T> = T | null | undefined
function safeGet<T>(value: Maybe<T>, fallback: T): T {
return value ?? fallback
}
Use TypeScript's advanced types:
// Discriminated unions for state machines
type RequestState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success', data: User[] }
| { status: 'error', error: string }
// Template literal types
type EventName = `on${Capitalize<keyof HTMLElementEventMap>}`
// Results in: 'onClick' | 'onMouseOver' | 'onSubmit' ...
Discriminated union type diagram
// Before — What does this return?
function calculate(x, y) {
// ... 50 lines of math
}
// After — You know exactly what to expect
function calculate(x: number, y: number): CalculationResult {
// ... implementation
}
With strict mode, renaming a property breaks everywhere it's used — which is exactly what you want. No silent failures.
VSCode's IntelliSense becomes psychic. Autocomplete shows you exactly what's available, never guessing wrong.
Before strict mode (3 months):
After strict mode (3 months):
unknown instead of any for gradual typingexactOptionalPropertyTypes for stricter optional handlingtsc --noEmit in CIDay 1: Enable strict: true and count errors
Day 2-3: Fix all function parameter/return types
Day 4: Handle null/undefined with guard clauses
Day 5: Run tsc --noEmit in CI
The first week hurts. The second week gets better. By week three, you'll wonder how you ever coded without it.
Strict TypeScript isn't about being pedantic — it's about catching bugs before your users do. And that's worth every error message.
Written by
Jordan Mercer
Keep Reading
A deep-dive into the architecture patterns, folder structures, and performance strategies that keep large Next.js codebases maintainable as they grow.
RSCs aren't just an optimization — they're a completely new way to think about data fetching and rendering. Here's the mental model that finally made it click.
After using Prisma on dozens of production apps, we've settled on a set of patterns for migrations, seeding, relations, and query optimization.
Let's work together