-
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
248: Practice Exercise: Building a Complete Typed API Layer End to End
Look, we've all been there. You're in a rush to get a feature shipped, you call an endpoint, and you just want the data on the screen. The temptation to use a type assertion—that little as User or as Project[]—is incredibly strong because it satisfies the compiler instantly. But here is the hard truth: as is essentially you telling TypeScript to shut up and trust you. The problem is that TypeScript doesn't exist at runtime, but your API definitely does.
The "Trust Me, I'm an Engineer" Approach
In a naive implementation, you might write a fetch wrapper that looks something like this. It feels clean, it's concise, and it gives you autocomplete in your components.
async function fetchProject(id: string): Promise<Project> {
const response = await fetch(`/api/projects/${id}`);
return response.json() as Project;
}
On the surface, this is great. You get a Project object back, and your IDE tells you exactly which properties are available. But you've created a "lying type." You aren't actually guaranteeing that the response matches the Project interface; you're just pretending it does. If the backend team decides to rename project_name to title, or if a field you thought was mandatory suddenly comes back as null, TypeScript won't warn you. Your app will simply crash with a Cannot read property 'x' of undefined in production, and you'll spend two hours debugging a "type-safe" application.
When the Boundary Breaks
The danger here is that the API boundary is the most volatile part of your system. You don't control the database, you might not control the backend, and even if you do, the network is a chaotic place. When we rely on as, we are pushing the point of failure deep into our UI components. A component might try to map over an array that is actually undefined because the API returned a 404 with an error object instead of a list.
I've seen this happen in huge enterprise apps where a single null value in a deeply nested API response takes down the entire dashboard. The "cost" of the naive approach is low upfront (it's fast to write), but the maintenance cost is astronomical because you can't actually trust your types.
Hardening the Boundary with Runtime Validation
The better way—the professional way—is to treat the API response as unknown until it is proven otherwise. We do this by moving the validation to the very edge of the application. I typically reach for a library like Zod for this, because it allows us to define a schema once and derive the TypeScript type from that schema automatically.
import { z } from 'zod';
const ProjectSchema = z.object({
id: z.string().uuid(),
name: z.string(),
status: z.enum(['active', 'archived', 'draft']),
dueDate: z.string().datetime(),
});
type Project = z.infer<typeof ProjectSchema>;
async function fetchProject(id: string): Promise<Project> {
const response = await fetch(`/api/projects/${id}`);
const data = await response.json();
// This is where the magic happens.
// If the data is wrong, it throws an error HERE, not in your UI.
return ProjectSchema.parse(data);
}
Now, we aren't guessing. If the API returns something weird, ProjectSchema.parse() will throw a descriptive error immediately. This transforms a mysterious runtime crash in a React component into a clear, catchable error at the network layer. You've essentially created a firewall for your data.
Balancing the Boilerplate Cost
I'll be honest with you: this is more work. You're writing a schema and an interface. You're adding a dependency. For a tiny hobby project, this might be overkill. But for anything that someone else relies on, the trade-off is a no-brainer. You trade a few minutes of boilerplate for the absolute certainty that if a function returns a Project, it actually is a Project.
When you build your API layer this way, your components become boring. And in software engineering, "boring" is a compliment. Boring components don't have to handle unexpected nulls or check if a property exists before using it; they just render the data they were promised.
📋 Practical Task
Exercise: Implementing a Validated Subscription Service
You are building a subscription management portal. The API is notorious for returning inconsistent data (sometimes missing fields, sometimes wrong types). Your goal is to build a type-safe service that prevents this "garbage data" from reaching the UI.
Requirements:
- Define a Zod schema for a
Subscriptionobject. It must have:id(string)plan(an enum of 'basic', 'premium', or 'enterprise')isActive(boolean)renewalDate(a valid ISO date string)
- Create a TypeScript type
Subscriptioninferred from that schema. - Implement a function
getSubscription(userId: string)that:- Fetches data from
/api/subscriptions/${userId}. - Parses the JSON response using your schema.
- Wraps the parsing logic in a
try/catchblock. If validation fails, it should throw a custom error:"API_CONTRACT_VIOLATION".
- Fetches data from
Test Case: Your code should be able to handle a response where plan is unexpectedly returned as "gold" (which is not in your enum) by throwing the specific API_CONTRACT_VIOLATION error rather than allowing the invalid object to be returned.
There are no comments for now.