Types Are Not Safety

I have nothing against TypeScript. What I have something against is using types as a security blanket. The type system is meant to help you work out what your data looks like and where your boundaries are. In a lot of teams it turns into a ritual instead: the more types, the more elaborate, the calmer everyone feels — as if a green tsc means the code is now “safe”. It isn’t, and it’s the kind of illusion that quietly slows your iteration down.

Types are for thinking, not for reassurance

Types are at their most useful when they force you to answer a few questions up front. What shape of data does this function take? Which fields genuinely exist, which are optional, how does the caller use what comes back? Writing those down as types lays your fuzzy assumptions out where you can inspect them, and that act is worth more than the type file it produces.

The trouble shows up at the other extreme. You open a file and get hit by a wall of generic gymnastics — a conditional type wrapping a mapped type, inferring a third layer out the bottom — and it takes ten minutes just to work out what the type is doing. Then you realise the whole pile describes “an object with three fields”. That isn’t thinking about data; that’s using the type system to prove you’re good at the type system.

My own test is simple: if a type needs a comment to be legible, it’s probably wrapping a design that was never thought through. A data model you’ve actually understood tends to come with simple types, because the shape itself is simple. The moment you’re reaching for conditional types to force an interface into place, that’s usually a sign the upstream structure wants re-cutting, not that the type needs to be cleverer. Complexity in the types is a symptom of a design that hasn’t converged, not a display of engineering skill.

Over-typing carries a quieter cost too: it slows iteration. Change a requirement and that finely-carved generic collapses, and now you’re spending your time coaxing the compiler back into a good mood instead of changing the business logic. Types should serve the speed at which you change code, not hold it hostage.

The boundary is the point: a green build doesn’t mean correct data

Here’s the bit that’s easiest to forget: TypeScript’s types don’t exist at runtime. They’re erased at compile time, and at execution there is nothing left checking anything for you. Declaring function f(u: User) does not mean the thing that actually reaches f at runtime is a User.

The instant data comes from outside — an API response, a user form, localStorage, a third-party webhook, a message queue — the types guarantee nothing. The backend slips in an extra null, renames a field, turns an optional into a required, and your User type never objects; tsc stays green, and production breaks somewhere you’d never have looked.

What actually holds the line is runtime validation at the boundary. Check the data with a schema validator the moment it enters the system — something like Zod or Valibot, where the schema produces the type, so the inner code consumes a value that’s already been verified:

import { z } from "zod";

const User = z.object({
  id: z.string().uuid(),
  name: z.string().min(1),
  email: z.string().email().optional(),
});

type User = z.infer<typeof User>;        // type derived from the schema
const user = User.parse(await res.json()); // runtime actually checks

Which library you pick matters less than the posture: put validation at the boundary so the interior can trust its data. A screen full of type annotations buys you compile-time self-consistency; the runtime parse is what buys you an execution-time guarantee. The first makes writing code smoother; the second keeps the system from silently rotting when dirty data arrives. Neither substitutes for the other, and the people treating types as safety have usually dropped the second one entirely.

A green build is not a sound design

A clean tsc tells you exactly one thing: your types are consistent with each other. It says nothing about whether the logic is right, whether the boundaries hold, or whether the data flow makes sense. Type self-consistency is a low bar — low enough that thoroughly wrong logic clears it without breaking stride.

I’ve seen too many PRs whose subtext is “the types pass, so it’s probably fine”. But the compiler only checks the small slice it can reach. Everything outside that slice — which is to say nearly everything that actually goes wrong: runtime data, the behaviour of external systems, the correctness of the business rules — it will not say a word about. Treating “it builds” as a quality guarantee badly overestimates what the tool covers.

The pragmatic landing is honestly dull: turn strict mode on, avoid any, lean on unknown plus narrowing — all correct. But don’t cast types as the lead. They’re a tool for reasoning about data and a partner for boundary validation, not a shield you hide behind and relax once it goes green. The type system is a tool, not a religion; the moment you start bending the design to make the types pretty, you’ve got it backwards.

Takeaways

  • The value of types is forcing you to work out the shape of your data and your boundaries, not making you feel safe.
  • Complexity in the types is usually a symptom of a design that hasn’t converged, not a show of skill.
  • TypeScript’s types don’t exist at runtime; external input can only be held at the boundary by a schema validator.
  • A green tsc means the types are self-consistent — nothing about whether the logic is right or the data is clean.
  • Turn strict mode on and avoid any, but treat types as a tool, not a religion, and don’t let them drive the design.

Sheng’s take, drafted with Claude · part of the 2026-06-13 blog renovation, paint still drying.