Back to Articles
Programming Feb 13, 2026 · 8 min read

Why I stopped trusting TypeScript at runtime

Daniel

Software Developer

TypeScript caught the bug at compile time. Great. But what happens when the API returns something unexpected at runtime? Production bug.

The Problem

We had this TypeScript code:

interface User {
id: number;
name: string;
email: string;
}

function processUser(user: User) {
// TypeScript guarantees user exists and has these fields
sendEmail(user.email, 'Welcome!');
}

Production returned: { id: null, name: undefined }

TypeScript didn’t save us. The API changed, but our code broke.

Solution: Runtime Validation

I use Zod for runtime validation:

import { z } from 'zod';

const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});

// Parse at API boundary
const user = UserSchema.parse(apiResponse);  // throws if invalid

Integration with Fastify/Express

app.get('/user/:id', async (req, res) => {
const user = await fetchUser(req.params.id);

// Validate before processing
const validated = UserSchema.safeParse(user);
if (!validated.success) {
return res.status(500).json({ error: 'Invalid data from upstream' });
}

// Now safe to use
processUser(validated.data);
});

The Pattern

My new approach:

  1. Define schemas with Zod (single source of truth)
  2. Generate TypeScript types from schemas
  3. Validate at every API boundary
  4. Trust but verify

Is It Worth It?

Extra code? Yes. Sleep better at night? Also yes. One time, a payment service returned { amount: "NaN" }. Zod caught it. TypeScript didn’t.