The Hidden Contract Between Your Types and Your Runtime

‎

Hatched by

May 16, 2026

6 min read

9999%

0

The question underneath every TypeScript validation choice

What are you really trusting when your code says a value is valid?

Most developers answer that question too quickly. They point to TypeScript, or to a validation library, or to an enum, and assume the problem is solved. But the deeper issue is not whether a value can be named in code. It is whether the name still means anything once the program is running.

That tension matters because TypeScript lives in two worlds at once. In one world, it is a compile time language of guarantees, narrowing, and static structure. In the other, it is just JavaScript, where every value arrives as messy reality: JSON payloads, query strings, environment variables, user input, database rows. The real design problem is not choosing between compile time and runtime. It is deciding how much truth each layer is allowed to claim.

A type that only exists while compiling is a promise to the programmer. A validator that exists at runtime is a promise to the program.

Once you see that distinction, two familiar topics suddenly become part of the same story: schema validation libraries and enum membership checks. Both are about drawing a boundary between what the compiler can help you believe and what the runtime can actually prove.


The seductive illusion of certainty

TypeScript is excellent at making incorrect code feel impossible. That is both its superpower and its trap. When you define an enum, a union, or a custom type, you create a compact vocabulary for your domain. When you add a validation library, you reinforce that vocabulary with an executable gatekeeper. The temptation is to assume the words and the gate are the same thing.

They are not.

Consider a simple domain like payment status:

enum PaymentStatus {
  Pending = "pending",
  Paid = "paid",
  Failed = "failed",
}

At a glance, this feels complete. You have a closed set of allowed values. Yet the moment a value comes from outside your code, you need to ask a harder question: is the input actually one of those values, or merely something that looks like it might be?

That is where runtime validation matters. A schema library does not just make data safer. It makes the boundary between trust and suspicion explicit. Instead of pretending a string is a valid status because it appears in a type annotation, you inspect it, narrow it, and only then proceed. This is especially important in TypeScript because the compiler cannot inspect your API payload, your configuration file, or your browser form at runtime.

The same logic applies to enums. An enum is a declaration of intent. A membership check is an act of verification. Confusing the two is like printing a map and assuming the terrain must cooperate.

This is why many teams eventually end up with both: enums or union types for expressing domain constraints, and schema validators for checking incoming data. But using both is only half the lesson. The deeper lesson is that not all constraints belong in the same layer.


Why enums are not the same as truth

Enums are appealing because they feel concrete. They compress a domain into a finite list. That gives you autocomplete, exhaustiveness checks, and better readability. But enums have a sharp edge: depending on how they are compiled, they may be more ghost than substance.

That becomes obvious with const enum. A const enum is erased during compilation. It is optimized for performance and inlining, which sounds great until you try to use it as a runtime object. The thing you thought was a list of values no longer exists in the emitted JavaScript. In other words, you can write code against a concept that vanishes before the program runs.

This is not a bug in TypeScript. It is a reminder that some abstractions are for the compiler only. const enum is a compression format for developer convenience, not a database of truth. If your code needs to inspect available values at runtime, then the abstraction has to survive compilation, or you need to recreate the information another way.

That leads to an important mental model: a type can describe a universe, but only runtime code can verify a planet is actually there.

Imagine an airport security system. A manifest of passengers is useful, but it does not prove who has arrived at the gate. The manifest can help the staff reason about expected travelers, yet the actual checkpoint still has to verify passports and boarding passes. In software, enums and TypeScript types are the manifest. Validation is the checkpoint.

The trap is thinking the manifest is enough just because it is precise.


The real tradeoff is not safety versus convenience

People often frame validation as a choice between strong correctness and developer ergonomics. That framing is too shallow. The real tradeoff is between static certainty and dynamic accountability.

Static certainty is cheap once you have it. The compiler can protect dozens of call sites at once. It can tell you when you forgot a case, when a property is missing, or when a function expects one shape and receives another. But static certainty is only valid inside the world the compiler can see.

Dynamic accountability is more expensive. You must write guards, parse inputs, and handle failure cases. Yet it is the only kind of certainty that survives contact with the outside world.

This is why schema validation libraries have become so central in TypeScript ecosystems. They bridge the gap between a static type system and runtime reality. Instead of saying, “this value should be a user,” they let you say, “this unknown value becomes a user only if it satisfies these checks.” That is a much stronger statement, because it acknowledges that data can lie.

A powerful way to think about this is through two verbs:

  1. Describe: Define what valid data looks like.
  2. Verify: Check whether received data matches that description.

TypeScript types are excellent at description. Validation libraries are excellent at verification. Problems begin when developers use description as if it were verification.

This distinction matters even more when your codebase grows. The larger the system, the more likely a value will cross trust boundaries: client to server, external API to internal service, env var to configuration, database to domain layer. Every boundary is a place where the program should stop assuming and start proving.


A better framework: three kinds of certainty

To make these ideas practical, it helps to split your code into three zones of certainty.

1. Compile time certainty

This is where TypeScript shines. You use unions, enums, generics, and discriminated types to model domain logic. Here, the goal is to catch mistakes before they ship.

Example: if your UI only supports three tabs, a union type can make it impossible to pass an invalid tab label to the rendering function.

type Tab = "overview" | "activity" | "settings";

This is compact, expressive, and easy to refactor. But it is not a runtime defense.

2. Runtime certainty

This is where input is suspicious by default. A user might send `

Sources

← Back to Library

Hatch New Ideas with Glasp AI 🐣

Glasp AI allows you to hatch new ideas based on your curated content. Let's curate and create with Glasp AI :)

Start Hatching 🐣