TypeScript
Completed
-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
72: Runtime Validation with Zod
A few years ago, I was reviewing a pull request for a teammate who had built a sleek integration with a third-party shipping API. The code looked pristine. He'd defined a comprehensive ShippingManifest interface, and every function used it perfectly. I remember thinking, "This is exactly why we use TypeScript."
Two days after the deploy, we got a critical alert. The API had started returning null for the tracking_number field on certain international orders—something the API documentation promised would always be a string. Because the TypeScript interface told the compiler the field was a string, the teammate hadn't written any manual checks for it. The app tried to call .toLowerCase() on that null value, and the entire checkout page crashed for about 5% of our users.
This is the "TypeScript Lie." We often forget that TypeScript is a compile-time tool. Once your code is transpiled to JavaScript and running in a browser or on a server, those interfaces vanish. They provide zero protection against malformed JSON, unexpected API changes, or user input. That's where Zod comes in.
Turning Schemas Into Type Guards
Zod allows you to define a schema—a runtime representation of your data—that can actually validate the data as it enters your system. Instead of just telling TypeScript "I expect this to be a User," you're telling the runtime "Verify that this is a User, and if it isn't, throw an error immediately."
Let's look at how we would have handled that shipping manifest issue. Instead of just an interface, we create a Zod schema:
import { z } from "zod";
const ShippingManifestSchema = z.object({
trackingNumber: z.string().min(1, "Tracking number cannot be empty"),
weight: z.number().positive(),
carrier: z.enum(["FedEx", "UPS", "DHL"]),
estimatedDelivery: z.string().datetime(),
});
Now, when the data arrives from the API, you don't just cast it using as ShippingManifest. Instead, you use the .parse() method. If the data doesn't match the schema, Zod throws a ZodError, allowing you to catch the failure at the boundary of your application rather than letting a null value seep deep into your business logic where it's harder to trace.
The Magic of Type Inference
One of the biggest headaches with runtime validation in the past was "Double Declaration." You'd have to write a validation function and then manually write a TypeScript interface that mirrored that validation. If you added a field to one, you'd inevitably forget the other, leading to the very bugs we're trying to avoid.
Zod solves this with z.infer. You define the schema once, and Zod extracts the TypeScript type from it automatically.
// No need to manually write 'interface ShippingManifest { ... }'
type ShippingManifest = z.infer<typeof ShippingManifestSchema>;
async function fetchManifest(id: string): Promise<ShippingManifest> {
const response = await fetch(`/api/shipping/${id}`);
const data = await response.json();
// This validates the data AND returns it typed as ShippingManifest
return ShippingManifestSchema.parse(data);
}
I personally prefer this approach because the schema becomes the "Single Source of Truth." If I change weight from a number to a string in the schema, TypeScript will immediately flag every single place in my codebase where I treated that weight as a number.
Handling Validation Failures Gracefully
While .parse() is great for when you're certain the data should be correct, it's a bit aggressive because it throws exceptions. In many cases—like validating a user-submitted form—you don't want your app to crash; you want to return a helpful error message.
For those scenarios, use .safeParse(). It returns an object indicating whether the validation succeeded or failed, without throwing an error.
const result = ShippingManifestSchema.safeParse(apiResponse);
if (!result.success) {
// result.error contains a detailed array of exactly which fields failed and why
console.error("Invalid API response:", result.error.format());
return handleApiError();
}
// Now 'result.data' is fully typed and guaranteed to be valid
console.log(result.data.trackingNumber);📋 Practical Task
Exercise: Building a Robust Product Inventory Validator
You are building an inventory management system. You receive product updates from a legacy CSV-to-JSON converter that is notoriously unreliable. Your task is to create a validation layer using Zod to ensure no "garbage" data enters your state.
Requirements:
- Create a Zod schema named
ProductSchemathat validates:id: A string that must be a UUID (usez.string().uuid()).name: A string with a minimum length of 3 characters.price: A number that must be greater than 0.category: A string that must be one of the following: "Electronics", "Clothing", or "Home".tags: An array of strings, where each string is at least 2 characters long.
- Use
z.inferto create aProducttype from the schema. - Write a function called
processProductUpdatethat takes anunknownvalue, usessafeParseto validate it against theProductSchema, and:- Returns the validated product if successful.
- Logs a specific error message and returns
nullif validation fails.
Test your implementation with these two cases:
const validProduct = {
id: "550e8400-e29b-41d4-a716-446655440000",
name: "Mechanical Keyboard",
price: 120.00,
category: "Electronics",
tags: ["gaming", "peripherals"]
};
const invalidProduct = {
id: "123-bad-id",
name: "Hi",
price: -10,
category: "Food",
tags: ["a"]
};There are no comments for now.