Other people get lost here. We start seeing <T>, interface, type, return T… everything mixed up and it looks like hieroglyphics.
Let’s untangle it step by step.
Generics in Interfaces vs Types
When to use one or the other? The syntax is almost identical.
// Option A: Generic Interface
interface BoxInterface<T> {
value: T;
}
// Option B: Generic Type
type BoxType<T> = {
value: T;
};
// They are used EQUALLY
const a: BoxInterface<string> = { value: "Hello" };
const b: BoxType<string> = { value: "Hello" };// Option A: Generic Interface
interface BoxInterface<T> {
value: T;
}
// Option B: Generic Type
type BoxType<T> = {
value: T;
};
// They are used EQUALLY
const a: BoxInterface<string> = { value: "Hello" };
const b: BoxType<string> = { value: "Hello" };There is no real functional difference for simple data. Use whatever you prefer (we usually use interface for objects and type for functions/unions).
The “Wrapper” Pattern (Datawrapper)
This is the real world use case #1: API Responses.
We have a fixed structure (data, status, error) but the content of data changes according to the call.
// Let's define the generic "shell"
interface ApiResponse<Data> {
status: number;
message: string;
data: Data; // 👈 Here goes the magic
}
// Let's define our concrete models
interface User { id: number; name: string; }
interface Product { sku: string; price: number; }
// Let's combine them!
type UserResponse = ApiResponse<User>;
type ProductResponse = ApiResponse<Product>;
function fetchUser(): UserResponse {
return {
status: 200,
message: "OK",
data: { id: 1, name: "AliceDev" } // TS knows that data is User
};
}// Let's define the generic "shell"
interface ApiResponse<Data> {
status: number;
message: string;
data: Data; // 👈 Here goes the magic
}
// Let's define our concrete models
interface User { id: number; name: string; }
interface Product { sku: string; price: number; }
// Let's combine them!
type UserResponse = ApiResponse<User>;
type ProductResponse = ApiResponse<Product>;
function fetchUser(): UserResponse {
return {
status: 200,
message: "OK",
data: { id: 1, name: "AliceDev" } // TS knows that data is User
};
}Type “Prop Drilling”
Sometimes we have Generics inside Generics. It’s like passing a variable from a grandparent to a grandchild.
interface PaginatedResponse<T> {
items: T[]; // Array of T
total: number;
page: number;
}
// ApiResponse contains PaginatedResponse, which contains User
type UsersApi = ApiResponse<PaginatedResponse<User>>;
// It seems complex, but TS resolves it alone:
// UsersApi = {
// status: number,
// data: {
// items: User[],
// total: number
// }
// }interface PaginatedResponse<T> {
items: T[]; // Array of T
total: number;
page: number;
}
// ApiResponse contains PaginatedResponse, which contains User
type UsersApi = ApiResponse<PaginatedResponse<User>>;
// It seems complex, but TS resolves it alone:
// UsersApi = {
// status: number,
// data: {
// items: User[],
// total: number
// }
// }Advanced: infer (Type Functions)
This is already black belt level, but just so you know it exists.
You can create a Type that “extracts” types from other types using infer.
// A type that extracts the type from inside an Array
type UnpackArray<T> = T extends (infer U)[] ? U : T;
type StringArray = string[];
type JustString = UnpackArray<StringArray>;
// Result: string// A type that extracts the type from inside an Array
type UnpackArray<T> = T extends (infer U)[] ? U : T;
type StringArray = string[];
type JustString = UnpackArray<StringArray>;
// Result: stringInsight: Type Functions
Do you realize what we just did? UnpackArray<T> is literally a function.
It receives an argument (T).
It does an if (extends).
It returns a result (U or T).
TypeScript’s type system IS a programming language in itself. 🤯
Why UnpackArray returns string
Detailed explanation of infer U:
When we write T extends (infer U)[], we are asking TS: “Does the type T look like an Array of something? If so, capture that ‘something’ and call it U”.
- We pass
string[]asT. - TS matches
string[]against(infer U)[]. - It deduces that
Umust bestring. - It returns
U(string).
Your turn!
Define two variables using the ApiResponse interface we just saw above.
acierto(success): Must be a correct response withdatabeing anumber(e.g., 42).fallo(failure): Must be a response withdatabeing astring(e.g., “Critical error”).