The Ultimate TypeScript Generics Tutorial with Examples for 2026
If you have been writing TypeScript for any amount of time, you have probably encountered the angle bracket syntax (<T>). At first glance, it can look like mathematical hieroglyphics. But once you understand how they work, generics become the most powerful tool in your TypeScript arsenal.
Welcome to our comprehensive typescript generics tutorial with examples. In this guide, we are going to strip away the academic jargon and break down generics into practical, copy-paste-ready concepts. Whether you are building API clients, React components, or complex data stores, mastering generics will elevate your code from “it works” to “it’s brilliantly type-safe.”
Why Do We Need Generics?
Before we dive into the “how,” let’s talk about the “why.”
Imagine you are building a function that returns the first item in an array. Without generics, you have two bad options:
Option 1: Use any
function getFirstItem(arr: any[]): any {
return arr[0];
}
This compiles, but you lose all type safety. If you pass an array of User objects, TypeScript has no idea that the returned item is a User. You lose autocompletion and compile-time error checking.
Option 2: Function Overloads
function getFirstItem(arr: string[]): string;
function getFirstItem(arr: number[]): number;
function getFirstItem(arr: any[]): any {
return arr[0];
}
This is incredibly rigid. What if someone passes an array of boolean values? Or an array of custom Product objects? You would have to write an endless list of overloads.
Generics solve this by acting as a variable for types. Instead of hardcoding the type, you let the caller tell the function what type it is working with.
Prerequisites
To get the most out of this tutorial, you should have:
* Node.js installed (v20 LTS or newer, as we approach 2026).
* TypeScript v5.x or higher installed globally or via npm.
* A solid understanding of basic TypeScript types (string, number, boolean, arrays, and basic interfaces).
* Familiarity with modern JavaScript (ES6+).
Understanding Generic Syntax: The <T> Convention
Let’s rewrite our getFirstItem function using generics.
function getFirstItem<T>(arr: T[]): T {
return arr[0];
}
What is <T>?
<T> is a type parameter. Just like a function parameter (name) accepts a value, <T> accepts a type.
By convention, TypeScript developers use single capital letters for type parameters. T stands for “Type”. Here are a few other common conventions:
* K for Key (in objects/maps).
* V for Value (in objects/maps).
* E for Element (in collections/iterables).
* R for Return (when dealing with complex promises or callbacks).
How to Call Generic Functions
When you call a generic function, you can explicitly pass the type argument inside angle brackets:
const numbers = [10, 20, 30];
const firstNumber = getFirstItem<number>(numbers);
// TypeScript knows firstNumber is a 'number'
const users = [{ name: "Alice" }, { name: "Bob" }];
const firstUser = getFirstItem<{ name: string }>(users);
However, modern TypeScript has incredibly powerful type inference. 99% of the time, you don’t even need to write the <number> part. TypeScript infers it automatically based on the arguments you pass in:
const numbers = [10, 20, 30];
const firstNumber = getFirstItem(numbers);
// Inferred as 'number'
const strings = ["hello", "world"];
const firstString = getFirstItem(strings);
// Inferred as 'string'
Step-by-Step: Working with Multiple Type Variables
Generics aren’t limited to just one type. You can use multiple type variables in a single function or class. This is highly useful when you are relating two distinct inputs.
A classic example is a map function or a function that merges two different objects into a tuple (array with fixed length and types).
function createTuple<X, Y>(first: X, second: Y): [X, Y] {
return [first, second];
}
// Explicit declaration
const mixedData = createTuple<string, boolean>("Is TypeScript great?", true);
// Type: [string, boolean]
// Inferred (TypeScript figures out it's [number, string])
const autoInferred = createTuple(42, "Universe");
Notice how X and Y capture the respective types and then enforce them on the return value [X, Y].
Using Generics with Interfaces and Classes
Generics truly shine when you start applying them to data structures. If you are building a React state container, a database ORM, or an API response wrapper, you will use generic interfaces and classes constantly.
Generic Interfaces
Let’s model a standard API response. An API usually returns a wrapper object with metadata, plus the actual data payload. The metadata structure is always the same, but the data payload changes depending on the endpoint.
interface ApiResponse<T> {
status: number;
message: string;
data: T;
timestamp: Date;
}
// Usage for a User endpoint
interface User {
id: string;
name: string;
email: string;
}
const userResponse: ApiResponse<User> = {
status: 200,
message: "Success",
data: {
id: "123",
name: "Jane Doe",
email: "jane@example.com"
},
timestamp: new Date()
};
// Usage for a Product endpoint
interface Product {
sku: string;
price: number;
}
const productResponse: ApiResponse<Product> = {
status: 200,
message: "Success",
data: { sku: "TS-BOOK", price: 29.99 },
timestamp: new Date()
};
Generic Classes
Let’s build a simple in-memory storage class. We want this storage to only hold items of a specific type once it’s initialized.
class DataStore<T> {
private items: T[] = [];
addItem(item: T): void {
this.items.push(item);
}
getItems(): T[] {
return [...this.items]; // Return a copy to prevent direct mutation
}
removeItem(index: number): void {
this.items.splice(index, 1);
}
}
// Create a store exclusively for strings
const stringStore = new DataStore<string>();
stringStore.addItem("Hello");
stringStore.addItem("World");
// stringStore.addItem(42); // ERROR: Argument of type 'number' is not assignable to parameter of type 'string'.
// Create a store for custom objects
const booleanStore = new DataStore<boolean>();
booleanStore.addItem(true);
Constraints: Taming Generic Types with extends
By default, a generic type T can be anything. This means TypeScript will only let you access properties that belong to every single object in JavaScript (like .toString()).
What if you want to write a function that takes an item and prints its length property?
// ERROR: Property 'length' does not exist on type 'T'.
function logLength<T>(item: T): void {
console.log(item.length);
}
TypeScript throws an error because T could be a number, and numbers don’t have a length property. To fix this, we use the extends keyword to add a constraint.
Using extends with Custom Interfaces
We tell TypeScript: “This generic type T must have at least a length property.”
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(item: T): void {
console.log(item.length); // Works perfectly!
return item; // We can also return the exact type passed in
}
logLength("Hello TypeScript"); // 5 (Strings have length)
logLength([1, 2, 3]); // 3 (Arrays have length)
// logLength(42); // ERROR: numbers don't have a length property
The keyof Operator: A Match Made in Heaven
When you combine extends keyof, you achieve TypeScript nirvana. The keyof operator takes an object type and produces a string or numeric literal union of its keys.
This is incredibly useful when you want to safely access properties of an object dynamically.
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
const user = {
id: 1,
name: "Alice",
role: "Admin"
};
const userName = getProperty(user, "name"); // Type is inferred as 'string'
const userId = getProperty(user, "id"); // Type is inferred as 'number'
// ERROR: Argument of type '"age"' is not assignable to parameter of type '"id" | "name" | "role"'.
const userAge = getProperty(user, "age");
In this example, K extends keyof T guarantees that the key you pass to the getProperty function actually exists on the object T. This prevents countless undefined runtime errors.
Default Type Parameters
In TypeScript 5.x and modern codebases, you can provide default types for generic parameters. This is fantastic for library authors who want to provide a sensible default while allowing users to override it if necessary.
interface PaginatedResponse<T, Meta = { total: number; page: number }> {
results: T[];
meta: Meta;
}
// We only specify the first type. Meta uses its default.
const standardResponse: PaginatedResponse<User> = {
results: [],
meta: { total: 0, page: 1 }
};
// We can override both if we have a custom metadata structure
interface CustomMeta {
total: number;
currentPage: number;
hasNextPage: boolean;
}
const customResponse: PaginatedResponse<Product, CustomMeta> = {
results: [],
meta: { total: 0, currentPage: 1, hasNextPage: false }
};
Generic Utility Types (TypeScript’s Secret Weapon)
TypeScript comes with built-in generic utility types that handle common type transformations. You will see these everywhere in 2026 codebases. Instead of writing your own, learn to leverage these:
Partial<T>
Makes all properties of T optional. Great for update/patch functions.
interface User {
id: string;
name: string;
email: string;
}
function updateUser(id: string, fields: Partial<User>) {
// Find user in database...
// Update only the provided fields...
}
updateUser("1", { name: "New Name" }); // We don't have to pass id or email
Omit<T, Keys>
Creates a new type by removing specific keys from T.
// We want to create a user, but the database generates the ID.
type CreateUserDTO = Omit<User, "id">;
// CreateUserDTO has 'name' and 'email', but no 'id'
const newUser: CreateUserDTO = {
name: "Bob",
email: "bob@example.com"
};
Pick<T, Keys>
The opposite of Omit. Selects only specific keys.
type UserSummary = Pick<User, "name" | "email">;
Readonly<T>
Makes all properties read-only.
const readOnlyUser: Readonly<User> = {
id: "1",
name: "Alice",
email: "alice@example.com"
};
// readOnlyUser.name = "Bob"; // ERROR: Cannot assign to 'name' because it is a read-only property.
Common Pitfalls and How to Avoid Them
Over the years, I’ve seen developers (including myself) stumble into the same traps when learning generics. Let’s look at how to avoid them.
Pitfall 1: Overusing Generics
Generics add cognitive load. If a function only ever takes a string, don’t write function process<T extends string>(input: T). Just write function process(input: string).
Rule of thumb: Only use generics when you need to relate two or more types (e.g., an input type and an output type), or when you are building highly reusable, agnostic data structures.