A full guide to mastering generics in TypeScript, from fundamentals to advanced patterns.
#1 - What is Generics ?
Generics are a fundamental feature in TypeScript (and many other statically typed languages) that allow you to write reusable, type‑safe code.
When you see
<T>(or<T, U>etc.) placed right after a function name and before the parameter list, it means the function is generic. The angle brackets declare type parameters that can be used throughout the function’s signature and body.
#2 - What is that <T> ?
-
<T>appears right after the function name, before the parentheses. It tells TypeScript “this function is generic, with a type parameter called T.” -
Inside the function,
Tis used to type the parameter value and the return type.
Example generic declaration
function identity<T>(value: T): T {
return value;
}
The <T> tells TypeScript: “This function has a type parameter T.”
The parameter value and the return type are both T, so the function preserves the exact type passed in.
#3 - What Generics solve ?
Without generics, you’re forced to choose between:
-
Using any – flexible but throws away all type information, making your code error‑prone.
-
Duplicating code – writing separate versions for each type, which is tedious and hard to maintain.
-
Overloading – acceptable for a few types, but doesn’t scale well.
Generics let you write a single, type‑safe piece of code that adapts to the types you use it with.
#4 - Rusable functions with Generics
Description: Here is a reusable functions that work with multiple types.
// ❌ Duplicate functions for each type
function identityNumber(value: number) {
return value;
}
function identityString(value: string) {
return value;
}
console.log(identityNumber(10));
// result: 10
console.log(identityString('hi'));
// result: "hi"
// ✅ Generic function
function identity<T>(value: T): T {
return value;
}
console.log(identity<number>(10));
// result: 10
console.log(identity<string>('hi'));
// result: "hi"
console.log(identity<boolean>(true));
// result: true
#5 - Generic Fetch Function
Description: Use generics to type REST API responses, avoiding repetitive fetch functions.
interface IUser {
id: number;
name: string;
}
interface IProduct {
id: number;
title: string;
}
interface IApiResponse<T> {
data: T;
status: number;
}
// ✅ Generic fetch function
async function fetchData<T>(url: string): Promise<IApiResponse<T>> {
const res = await fetch(url);
const data: T = await res.json();
return { data, status: res.status };
}
// Real-world usage
const usersResponse = await fetchData<IUser[]>('/v1/api/users');
console.log(usersResponse.data);
// result: [{id:1,name:"Jhon"}, {id:2,name:"Sara"}]
const productsResponse = await fetchData<IProduct[]>('/v1/api/products');
console.log(productsResponse.data);
// result: [{id:1,title:"Laptop"}, {id:2,title:"Phone"}]
#6 - Generic Functions
Description: Reuse functions for different data types without repeating code.
// ✅ Generic function
function firstElement<T>(arr: T[]): T {
return arr[0];
}
const firstUserItem = firstElement(usersResponse.data);
console.log(firstUserItem);
// result: {id:1,name:"Jhon"}
const firstProductItem = firstElement(productsResponse.data);
console.log(firstProductItem);
// result: {id:1,title:"Laptop"}
#7 - Type Inference with Generics
Description: TypeScript can infer types automatically from generic functions, reducing annotations.
const users = await fetchData<IUser[]>('/v1/api/users');
console.log(users.data[0].name);
// result: "Jhon"
#8 - Multiple Generic Types
Description: Create functions that can accept multiple types, useful for key-value pairs.
function pair<K, V>(key: K, value: V) {
return { key, value };
}
const userPair = pair<string, IUser>('admin', users.data[0]);
console.log(userPair);
// result: {key:"admin", value:{id:1,name:"Jhon"}}
const productPair = pair<number, IProduct>(1, productsResponse.data[0]);
console.log(productPair);
// result: {key:1, value:{id:1,title:"Laptop"}}
#9 - Generic Constraints
Description: Limit generics to types with specific properties, like length.
function logLength<T extends { length: number }>(value: T) {
console.log(value.length);
}
logLength(usersResponse.data);
// result: 2
logLength('Hello World');
// result: 11
#10 - Generic Classes
Description: Use generics in classes for reusable storage or container patterns.
class Storage<T> {
private items: T[] = [];
add(item: T) {
this.items.push(item);
}
getAll(): T[] {
return this.items;
}
}
const userStorage = new Storage<IUser>();
userStorage.add(usersResponse.data[0]);
console.log(userStorage.getAll());
// result: [{id:1,name:"Jhon"}]
const productStorage = new Storage<IProduct>();
productStorage.add(productsResponse.data[0]);
console.log(productStorage.getAll());
// result: [{id:1,title:"Laptop"}]
#11 - Default Generic Types
Description: Provide default types for generics to simplify usage when a type is not specified.
interface IApiResponseDefault<T = string> {
data: T;
status: number;
}
const response1: IApiResponseDefault = { data: 'ok', status: 200 };
console.log(response1.data);
// result: "ok"
const response2: IApiResponseDefault<IUser> = { data: usersResponse.data[0], status: 200 };
console.log(response2.data);
// result: {id:1,name:"Jhon"}
#12 - Conditional Generics
Description: Use conditional types to select different types based on input types.
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<IUser>; // false
#13 - Mapped & Utility Types
Description: Modify existing types or create new types dynamically using mapped and utility types.
type PartialUser = Partial<IUser>;
type ReadonlyUser = Readonly<IUser>;
type UserMap = Record<string, IUser>;
const userMap: UserMap = {
u1: usersResponse.data[0],
u2: usersResponse.data[1],
};
console.log(userMap);
// result: {u1:{id:1,name:"Jhon"}, u2:{id:2,name:"Sara"}}
#14 - Recursive & Deep Generics
Description: Define recursive types, useful for trees or nested structures.
interface ITree<T> {
value: T;
children?: ITree<T>[];
}
const userTree: ITree<IUser> = {
value: usersResponse.data[0],
children: [{ value: usersResponse.data[1] }],
};
console.log(userTree);
// result: {value:{id:1,name:"Jhon"}, children:[{value:{id:2,name:"Sara"}}]}
#15 - Repository Pattern
Description: Create reusable repository classes to manage collections of items.
class Repository<T> {
private items: T[] = [];
add(item: T) {
this.items.push(item);
}
getAll(): T[] {
return this.items;
}
}
const userRepo = new Repository<IUser>();
userRepo.add(usersResponse.data[0]);
console.log(userRepo.getAll());
// result: [{id:1,name:"Jhon"}]
#16 - Generic Cache
Description: Implement a generic cache class to store and retrieve any type safely.
class Cache<T> {
private store = new Map<string, T>();
set(key: string, value: T) {
this.store.set(key, value);
}
get(key: string): T | undefined {
return this.store.get(key);
}
}
const userCache = new Cache<IUser>();
userCache.set('user1', usersResponse.data[0]);
console.log(userCache.get('user1'));
// result: {id:1,name:"Jhon"}
#17 - Real REST API Examples (Production-ready Singleton + Resource Wrappers)
Description: Perform typed REST API calls using a singleton generic REST client and resource-specific API wrappers, making endpoints clean, type-safe, and maintainable.
// --- Types ---
interface IUser {
id: number;
name: string;
}
interface IProduct {
id: number;
title: string;
}
interface IApiResponse<T> {
data: T;
status: number;
}
// --- Generic REST Client ---
class RestClient {
constructor(private baseUrl: string) {}
// Helper to build full URL
private buildUrl(endpoint: string) {
return `${this.baseUrl}${endpoint}`;
}
async get<T>(endpoint: string): Promise<IApiResponse<T>> {
const res = await fetch(this.buildUrl(endpoint));
const data: T = await res.json();
return { data, status: res.status };
}
async post<U, T>(endpoint: string, body: U): Promise<IApiResponse<T>> {
const res = await fetch(this.buildUrl(endpoint), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const data: T = await res.json();
return { data, status: res.status };
}
async put<U, T>(endpoint: string, body: U): Promise<IApiResponse<T>> {
const res = await fetch(this.buildUrl(endpoint), {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const data: T = await res.json();
return { data, status: res.status };
}
async delete<T>(endpoint: string): Promise<IApiResponse<T>> {
const res = await fetch(this.buildUrl(endpoint), { method: 'DELETE' });
const data: T = await res.json();
return { data, status: res.status };
}
}
// --- Singleton Client ---
// ✅ baseUrl could come from a config or .env file, e.g. process.env.API_BASE_URL
export const apiClient = new RestClient(
process.env.API_BASE_URL || 'https://api.example.com/v1/api'
);
// --- User API Wrapper ---
export class UserApi {
constructor(private client = apiClient) {}
getAll() {
return this.client.get<IUser[]>('/users');
}
create(user: Omit<IUser, 'id'>) {
return this.client.post<Omit<IUser, 'id'>, IUser>('/users', user);
}
update(user: IUser) {
return this.client.put<IUser, IUser>(`/users/${user.id}`, user);
}
delete(id: number) {
return this.client.delete<{ message: string }>(`/users/${id}`);
}
}
// --- Product API Wrapper ---
export class ProductApi {
constructor(private client = apiClient) {}
getAll() {
return this.client.get<IProduct[]>('/products');
}
create(product: Omit<IProduct, 'id'>) {
return this.client.post<Omit<IProduct, 'id'>, IProduct>('/products', product);
}
update(product: IProduct) {
return this.client.put<IProduct, IProduct>(`/products/${product.id}`, product);
}
delete(id: number) {
return this.client.delete<{ message: string }>(`/products/${id}`);
}
}
// --- Instantiate APIs ---
const usersApi = new UserApi();
const productsApi = new ProductApi();
// --- GET ---
const allUsers = await usersApi.getAll();
console.log(allUsers.data);
// result: [{id:1,name:"Jhon"}, {id:2,name:"Sara"}]
const allProducts = await productsApi.getAll();
console.log(allProducts.data);
// result: [{id:1,title:"Laptop"}, {id:2,title:"Phone"}]
// --- POST ---
const newUser = await usersApi.create({ name: 'Jhon' });
console.log(newUser.data);
// result: {id:3,name:"Jhon"}
const newProduct = await productsApi.create({ title: 'Tablet' });
console.log(newProduct.data);
// result: {id:3,title:"Tablet"}
// --- PUT ---
const updatedUser = await usersApi.update({ id: 3, name: 'Jhon Updated' });
console.log(updatedUser.data);
// result: {id:3,name:"Jhon Updated"}
const updatedProduct = await productsApi.update({ id: 3, title: 'Tablet Pro' });
console.log(updatedProduct.data);
// result: {id:3,title:"Tablet Pro"}
// --- DELETE ---
const deleteUser = await usersApi.delete(3);
console.log(deleteUser.data);
// result: { message: "User deleted successfully" }
const deleteProduct = await productsApi.delete(3);
console.log(deleteProduct.data);
// result: { message: "Product deleted successfully" }
#18 - Best Practices
Description: Guidelines for writing clean, reusable, and safe generic code.
function mapArray<TItem, UResult>(arr: TItem[], fn: (item: TItem) => UResult): UResult[] {
return arr.map(fn);
}
function logLength<T extends { length: number }>(value: T) {
console.log(value.length);
}
type Partial<T> = { [K in keyof T]?: T[K] };
#19 - Summary
Description: Generics make TypeScript code reusable, type-safe, and maintainable, especially for REST API and data handling.
- ❌ Without generics → repetitive, verbose, error-prone
- ✅ With generics → reusable, type-safe, maintainable
Examples included: fetch, storage, cache, repository, arrays, trees.