TypeScript
Validação de Dados
Zod
Qualidade de Software
Arquitetura

Data Validation with Zod: Why TypeScript Types Aren't Enough

TypeScript types are compilation guarantees, not execution guarantees. Schema-first validation with Zod creates a single source of truth between type and data.

There is a costly misunderstanding in teams that have adopted TypeScript enthusiastically. The belief that by having types, the system is protected against malformed data. It is not. And this confusion between what the compiler does and what it doesn't is the origin of an entire category of bugs that only appear in production.

I want to dispel this misunderstanding directly, because it shapes architectural decisions. Anyone who understands where types end begins to treat the system's boundaries with the care they require.

What TypeScript really guarantees

TypeScript is a type system that lives entirely at compile time. You write the annotations, the compiler checks them, and then they are deleted. The JavaScript that runs in production does not know that that object was a Usuario. For the execution engine, it's just any object.

This is by design, not a flaw. TypeScript was built to impose no cost at runtime. The consequence is that every type guarantee is a promise about the code you wrote, not the data it will process.

As long as the data is born within your code, the promise holds. A function that takes a number and returns a number is protected by the compiler from beginning to end. The problem starts when the data comes from outside.

The border where the guarantee evaporates

Think about everything that enters your system without being created by it. The response from an external API. The body of an HTTP request. A form filled out by a distracted human. A line read from database after a botched migration. An environment variable. A message in a queue.

At all of these points, you typically do something along the lines of declaring that the data you receive is of a certain type. An assertion. And here lies the mistake: this assertion does not verify anything. It just tells the compiler to trust you and move on. If the API changed a field from number to text, TypeScript continues to believe that it is a number, because you told it to believe it.

The result is a system that looks typed but has holes right at the edges, where the real world enters. The error does not happen at the border, where it would be easy to diagnose. It happens three layers deep, when something tries to use that field as if the promise were true. The trail goes cold, the stack trace points to the wrong place, and someone loses their afternoon.

Validation at runtime is what was missing

The conceptual solution is simple to state and easy to defer: at entry points, you need to actually check, at runtime, that the data has the shape you expect. Don't trust, check.

Runtime validation means code that effectively looks at data and responds to whether it is valid or not. If the API promised a number and sent a text, validation screams there, at the edge, with a clear message, before the value contaminates the rest of the flow. You trade a silent, profound bug for a noisy, localized failure.

Historically, this was laborious and easy to forget. We wrote the TypeScript interface on one side and, on the other, a separate validation function that checked the same fields by hand. Two descriptions of the same thing, held by different people, at different times. They differed. They always differ. The type said one thing, the validator checked another, and the actual data followed a third rule.

Zod and the schema-first approach

This is where Zod changes the way we think about the problem. The central idea is to reverse the order: instead of writing the type and then a validator that tries to keep up with it, you write a single schema, and the type is derived from it automatically.

You describe the form of the data once, with its rules, required fields, formats and limits. From this schema, Zod extracts two things at the same time. One is the validator that runs in production and actually examines the data. The other is the TypeScript] static type, generated from the same definition, without you having to write it by hand.

This is the gain that matters: a single source of truth. Type and validation can no longer diverge, because they come from the same place. If you change the schema, the type changes along with it, and any code that depended on the old format will fail to compile. Divergence is no longer possible by construction, rather than being avoided by discipline.

The external data comes in, passes through the schema, and comes out the other side as a value that the compiler can now treat with legitimate trust, because the trust was earned at runtime and not just declared. The blind assertion becomes a real check, and from then on the rest of the system is protected by types again.

What does this change in the mind of those who decide

Adopting schema-first validation is not a choice of tool, it is a choice of where to place the trust boundary. The rule I propose to the team is straightforward: nothing that comes from outside enters without going through a schema. API, form, bank, queue, configuration. Everything validates at the edge.

Within this boundary, you trust the types completely, because they have returned to correspond to reality. Outside of it, you don't trust anything that hasn't been verified. This clear separation between the validated territory and the wild is what makes the system predictable. Anyone who works with TypeScript in advanced projects knows that type is a reasoning tool, and it only reasons well about data that actually has the promised form.

It is worth recording the cost, which is honest and small. There is an initial effort to model the schemas and an execution overhead for validating data at the edges. In return, you eliminate a class of errors that are expensive precisely because they manifest late and far from the cause. It's one of the best returns on investment I know of in software quality, and speaks directly to broader web application security practices, as validating input is also the first line of defense.

If you lead a team that has adopted TypeScript but still treats external data with blind assertions, it's worth reviewing the system boundaries this week. The cost of adding validation where the data comes in is low, and the cost of not having it is exactly the kind of bug that no one wants to debug on Fridays.

Also read