- Generics are simply type-level parameters that let you pass data shapes into functions and interfaces instead of resorting to
any. - Built-in utility types like
Partial,Pick,Omit, andRecordprevent endless interface duplication across your codebase. - Wrapping native
fetchwith generic response envelopes ensures your React hooks and state management remain fully typed end-to-end. - Compile-time types disappear in the browser, making runtime type guards essential before trusting external JSON payloads.
Why do TypeScript generics look so scary at first?
Walk onto any floor in a Chennai IT park around 4 PM, and you will spot a frontend engineer staring at a screen filled with angle brackets. You see <T>, <K extends keyof T>, and Promise<ApiResponse<T>> stacked like a puzzle. The immediate temptation is to slap any on the variable, close the laptop, and head downstairs for filter coffee.
That fear comes from how tutorials teach generics. They show abstract math examples or build bizarre box-stacking classes nobody ever writes at work. In reality, generics solve one everyday problem: writing code that works with multiple types without throwing away safety.
Think of normal function arguments. You pass a customer name into a function, and the function operates on that specific string. Generics work the exact same way, but for types. Instead of passing a value into a parameter, you pass a type into an angle bracket. Once you see it powering an actual network payload, the syntax clicks instantly.
How do generic functions and interfaces work in real code?
Most backend microservices wrap their responses in a standard JSON envelope. Whether you fetch a Flipkart delivery tracking record or a user profile, the top-level keys look identical: a status code, a message string, and a data key containing the actual payload.
Without generics, you would either write twenty nearly identical interfaces or set data: any. Setting any completely defeats the purpose of running TypeScript in your repository. Generics let you create a single envelope interface that accepts the data shape dynamically.
// A reusable generic envelope for any backend endpoint
interface ApiResponse<TData> {
success: boolean;
message: string;
timestamp: number;
data: TData;
}
// Specific domain models
interface DeliveryStatus {
trackingId: string;
carrier: string;
estimatedDeliveryDays: number;
}
// Generic helper that wraps raw data into the envelope
function formatApiResponse<T>(payload: T, message: string): ApiResponse<T> {
return {
success: true,
message,
timestamp: Date.now(),
data: payload,
};
}
// TypeScript automatically infers T as DeliveryStatus here
const trackingData: DeliveryStatus = {
trackingId: "TN-EXP-9081",
carrier: "BlueDart",
estimatedDeliveryDays: 2,
};
const response = formatApiResponse(trackingData, "Status fetched");
console.log(response.data.carrier); // Autocomplete knows this is a string
The compiler reads formatApiResponse(trackingData), sees that trackingData matches DeliveryStatus, and assigns T to that shape. You do not even have to manually type the angle brackets on the function call. The autocompletion and safety follow the payload wherever it travels.
When should you use Partial, Pick, Omit, and Record?
Once your base interfaces exist, API requirements will constantly shift. Your signup endpoint requires name, email, phone, and password. But your profile update endpoint only updates whatever field the user touched in their account settings.
Instead of creating an entirely new UpdateUserProfileInput interface and copying five fields over, TypeScript provides built-in generic utility types. These take an existing interface, transform its properties, and return a clean new type.
interface UserProfile {
id: string;
name: string;
email: string;
phone: string;
loyaltyPoints: number;
role: "customer" | "merchant" | "admin";
}
// 1. Partial: Makes every property optional (ideal for PATCH requests)
type ProfileUpdateRequest = Partial<UserProfile>;
// 2. Pick: Extracts only the fields you specify
type PublicCardProps = Pick<UserProfile, "name" | "loyaltyPoints">;
// 3. Omit: Strips out sensitive or immutable fields
type CreateUserPayload = Omit<UserProfile, "id" | "loyaltyPoints">;
// 4. Record: Builds a dictionary with typed keys and values
type UserRoleDictionary = Record<string, UserProfile>;
const roleRegistry: UserRoleDictionary = {
"USR_001": {
id: "USR_001",
name: "Suresh Kumar",
email: "[email protected]",
phone: "9876543210",
loyaltyPoints: 450,
role: "customer",
},
};
Combining these utilities keeps your codebase DRY. If a backend engineer adds an isAadhaarVerified flag to UserProfile tomorrow, your Partial and Omit types inherit the change automatically without manual edits.
How do you type real API fetch responses?
JavaScript's native fetch method returns a Promise<Response>, and calling response.json() returns Promise<any>. Leaving that any in your networking layer allows subtle bugs to slip past your build pipelines unnoticed.
Building a lightweight, generic API client solves this cleanly. You pass your expected payload type to the client call, and the promise resolves directly to that type.
interface PaymentOrder {
orderId: string;
amountInPaise: number;
currency: "INR";
status: "created" | "attempted" | "paid";
}
// Generic HTTP fetch wrapper
async function apiRequest<TResponse>(endpoint: string): Promise<TResponse> {
const res = await fetch(endpoint);
if (!res.ok) {
throw new Error(`Network error: ${res.status} ${res.statusText}`);
}
const body: TResponse = await res.json();
return body;
}
// Consuming the API client
async function loadOrder(id: string) {
const order = await apiRequest<PaymentOrder>(`/api/orders/${id}`);
// TypeScript knows order has amountInPaise and status
const formattedAmount = (order.amountInPaise / 100).toFixed(2);
console.log(`Order ${order.orderId}: ₹${formattedAmount}`);
}
By enforcing the response type at the network boundary, your UI components receive fully typed objects. If someone mistypes a property name on order, the build fails before code ever reaches staging.
How do narrowing and type guards stop runtime bugs?
Remember that TypeScript types vanish the moment your code compiles to JavaScript. If your backend service returns a 500 error page with an HTML body or an unexpected error JSON instead of your PaymentOrder, TypeScript cannot prevent a runtime crash on its own.
This is where type guards and narrowing come in. A type guard uses standard runtime JavaScript checks, but tells the TypeScript compiler that an unknown object definitely matches a specific interface using the parameter is Type syntax.
interface ApiErrorPayload {
code: string;
message: string;
}
// Custom Type Guard function
function isApiError(data: unknown): data is ApiErrorPayload {
return (
typeof data === "object" &&
data !== null &&
"code" in data &&
"message" in data
);
}
// Using narrowing to handle mixed network responses
async function handleServerData(responseBody: unknown) {
if (isApiError(responseBody)) {
// Compiler knows responseBody has code and message inside this block
console.error(`Backend Error [${responseBody.code}]: ${responseBody.message}`);
return;
}
console.log("Valid payload received, processing normally");
}
This pattern bridges the boundary between what the TypeScript compiler assumes and what the browser actually receives over the wire.
Which common TypeScript mistakes trigger runtime crashes?
The most common blunder is relying on type assertions with as to force shapes. Writing const user = data as UserProfile tells the compiler to look away and trust you. If the API returns null or missing fields, your app crashes on user.name at runtime.
Another frequent trap is over-genericizing code. Creating functions like function print<T, K, V>() for a basic button component creates unreadable files without adding real type safety. Use generics only when the output type strictly depends on the input type.
Developers also struggle with default types on generic parameters. If a generic parameter has a sensible fallback, provide it. Writing interface TableProps<T = Record<string, unknown>> allows consumers to use the component without declaring types if they only want basic default behavior.
Where does this come up in Indian tech interviews?
Whether you interview at Razorpay, Swiggy, Zoho, or service firms pitching for Tier-1 enterprise deals, modern TypeScript screening rounds have stopped asking basic syntax questions. Tech leads want to see how you handle asynchronous state and shared component libraries.
Expect interviewers to hand you an untyped Axios or Fetch wrapper and ask: "How do we make this reusable for our checkout flow while guaranteeing type safety?" If you immediately suggest an ApiResponse<T> generic interface, pair it with Partial for patch updates, and mention a runtime type guard, you prove you build software designed for production scale.
What should you try coding right now?
Open an empty file in your editor or TypeScript playground. Write an in-memory cache function called createCache<T>. It should store items under a string key alongside an expiration timestamp, and provide two methods: set(key: string, value: T, ttlMs: number) and get(key: string): T | null.
Once you make the return type of get() correctly reflect the generic value without casting, you have mastered the core mechanics of generics.




Comments (0)
Be the first to comment!