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
77: Building a Type-Safe API Client
If you've spent any time calling APIs in TypeScript, you've probably written this line of code: const data = (await response.json()) as User;. It feels right. You know the API returns a User object, you've defined a User interface, and the type assertion as User makes the red squiggly lines go away. You feel type-safe.
The "as MyType" Illusion
Here is the cold truth: as User isn't providing safety; it's providing a blindfold. When you use a type assertion, you aren't telling TypeScript to verify that the data is a User; you're telling TypeScript to shut up and trust you. I've seen this crash production systems countless times. The API changes a field from userName to handle, the server sends back a 404 error page in HTML instead of JSON, or a field you thought was a number arrives as a string. TypeScript will happily let you call data.userName.toUpperCase() because you asserted it was a User, and then the app blows up with TypeError: Cannot read property 'toUpperCase' of undefined.
// This is the "dangerous" way async function getUser(id: string) { const response = await fetch(`/api/users/${id}`); // We are lying to the compiler here. // If the API returns { error: "Not Found" }, this still claims to be a User. return (await response.json()) as User; }Designing a Generic Request Wrapper
To build a truly type-safe client, we need to move the type logic out of the individual call and into a reusable wrapper. Instead of casting at the call site, we use generics to define the "shape" of the expected response. This allows us to centralize error handling and response parsing in one place, rather than repeating the same dangerous patterns in every single API function.
I prefer building a small
ApiClientclass. It keeps the base URL and headers in one spot and leverages a generic method to handle the heavy lifting.interface ApiResponse<T> { data: T; status: number; } class ApiClient { private baseUrl: string; constructor(baseUrl: string) { this.baseUrl = baseUrl; } // Notice the <T>. This means "I don't know what the data is yet, // but whoever calls this method will tell me." async request<T>(endpoint: string, options?: RequestInit): Promise<T> { const response = await fetch(`${this.baseUrl}${endpoint}`, options); if (!response.ok) { throw new Error(`API Error: ${response.status} ${response.statusText}`); } // We still have to cast here once, but it's centralized. // In a production app, I'd use a library like Zod here to // actually validate the runtime data. return response.json() as Promise<T>; } }Implementing Feature-Specific Services
Now that we have a generic engine, we don't want our UI components talking directly to the
ApiClient. That would leak implementation details everywhere. Instead, we create "Service" classes. This is where we map our business domains (like Products or Orders) to the generic request method.Let's say we're building a Product Catalog. We define our domain models and then use the client to fetch them.
interface Product { id: string; name: string; price: number; category: 'electronics' | 'clothing' | 'home'; } class ProductService { constructor(private client: ApiClient) {} async getProduct(id: string): Promise<Product> { // We pass the Product type into the generic request method. // Now, the return type of this function is automatically Promise<Product>. return this.client.request<Product>(`/products/${id}`); } async listByCategory(category: string): Promise<Product[]> { return this.client.request<Product[]>(`/products?cat=${category}`); } } // Usage const api = new ApiClient('https://api.mystore.com'); const products = new ProductService(api); const item = await products.getProduct('p123'); // 'item' is now correctly typed as Product. No 'as' needed in the business logic.By structuring it this way, you've created a clear pipeline:
Raw Network Response → Generic Wrapper → Typed Service → UI. If the API structure changes, you update the interface in one place, and TypeScript will immediately point out every single line of code in your entire application that is now broken.
📋 Practical Task
Refactoring the Legacy Order Management Client
You've inherited a codebase where the API calls are a mess of any types and inline casts. Your goal is to refactor the OrderClient to use a type-safe generic approach.
Current Broken Code:
interface Order { id: string; total: number; items: OrderItem[]; } interface OrderItem { productId: string; quantity: number; } class OrderClient { async getOrder(id: string) { const res = await fetch(`/api/orders/${id}`); return (await res.json()) as any; // This is the problem! } }Your Task:
- Create a
BaseClientclass with a genericrequest<T>` method that handles thefetchcall, checksresponse.ok, and returns the JSON as typeT.- Refactor the
OrderClientto extendBaseClient(or take it as a dependency).- Update the
getOrdermethod so that it no longer usesanyand correctly returns aPromise<Order>by passing the type to the generic request method.- Implement a new method
getOrderItems(orderId: string)that returns aPromise<OrderItem[]>.
There are no comments for now.