6 Sept 2026•4 min read
Most codebases stop at typed function signatures and call it type safety. The value is in the boundaries: the network, the database, and everything a user can type.
1 September 2026•3 min read
TypeScript adoption is effectively universal now, and yet a large share of production bugs in typed codebases are type errors in disguise. The reason is consistent: teams type the code they wrote and trust the data that arrives. Everything interesting enters your program at a boundary, and boundaries are where any tends to live under a different name.
There are three declarations that quietly turn off type checking while looking like type safety.
await res.json() typed as your response interface. The server can return anything, including an error page.process.env.SOMETHING treated as a string. It is string | undefined, and in production it is undefined.Each of these is a runtime value asserted into a compile-time shape. The compiler believes you. Users find out later.
The fix is to validate at the edge with a schema and derive the static type from that schema, so there is exactly one definition and it is enforced at runtime. Do it once, at the boundary, then let the inferred types flow inwards.
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
displayName: z.string().min(1),
});
type User = z.infer<typeof UserSchema>;
export async function fetchUser(id: string): Promise<User> {
const res = await fetch(url);
if (!res.ok) throw new ApiError(res.status);
return UserSchema.parse(await res.json());
}
The cost is one parse per request. The benefit is that a backend field rename becomes a loud error at the boundary with a precise path, instead of an undefined read three components deep during a user's session.
Validate configuration at startup and export a typed object. A process that refuses to boot with a missing variable is infinitely better than one that boots and fails on the first request that needs it. This takes fifteen minutes and removes an entire class of deployment incident.
The highest-leverage typing work is not annotating more, it is modelling better. A component with isLoading, data, and error as three independent optional fields has eight possible combinations, most of them nonsense, and your rendering logic has to defend against all of them. A discriminated union has three, all meaningful:
type Result<T> =
| { status: "loading" }
| { status: "error"; error: Error }
| { status: "ready"; data: T };
Now the compiler forces you to handle each case, and the impossible ones cannot be written. This is where types stop being documentation and start being design.
Turn on strict, obviously, but the two settings that catch real bugs and get resisted the most are noUncheckedIndexedAccess and exactOptionalPropertyTypes. The first makes array access honest about the fact that indexes can miss. The second stops undefined from silently satisfying an optional property. Both produce a wave of errors on adoption, and nearly all of them are real.
A type system's value is proportional to how much of your untrusted input it actually sees. Everything else is autocomplete.
Pick your single most-used API response and parse it. Then your environment config. Then the one state machine in your app that everybody is scared to edit. Three changes, a couple of days, and the class of bug that dominates your incident log starts showing up at build time instead.
@umarrafique923
Author and writer at CandyWrite. Sharing knowledge, tutorials, and reflections on technology, design, and ideas.
Join 12,000+ readers getting our Saturday morning editorial dispatch with our top essays and reading recommendations.
6 Sept 2026•4 min read
3 Sept 2026•3 min read
5 Sept 2026•4 min read
8 Sept 2026•5 min read
Discussion (0)
Join the conversation. Sign in to leave a response or reply to comments.