Ever felt like you're writing the same function over and over, just with different types? Welcome to the world of TypeScript generics – where code reusability meets type safety in perfect harmony.
What Are Generics?
Imagine you're building a toolbox. Instead of having separate hammers for nails, screws, and pins, wouldn't it be amazing to have one universal hammer that adapts to whatever you're working with? That's exactly what generics do for your code.
Generics allow you to create reusable components that work with multiple types while maintaining type safety. Think of them as type variables – placeholders that get filled in when you actually use the function, class, or interface.
The Problem Generics Solve
Let's start. Say you want to create a function that returns the first item from an array:
function getFirstString(items: string[]): string {
return items[0];
}
function getFirstNumber(items: number[]): number {
return items[0];
}
function getFirstBoolean(items: boolean[]): boolean {
return items[0];
}
This approach is like having a separate key for every door in your house. It works, but it's unnecessarily complicated.
Enter Generics: The Master Key 🗝️
Here's how generics solve this elegantly:
function getFirst<T>(items: T[]): T {
return items[0];
}
// Now we can use it with any type!
const firstString = getFirst(['apple', 'banana', 'cherry']); // Type: string
const firstNumber = getFirst([1, 2, 3]); // Type: number
const firstUser = getFirst([{name: 'Alice'}, {name: 'Bob'}]); // Type: {name: string}
The <T>
is our type parameter – a placeholder that TypeScript fills in based on how we use the function. It's like saying "Hey TypeScript, I'll tell you what type this is when I actually use it."
Generic Syntax: Breaking It Down
The basic syntax follows this pattern:
function functionName<TypeParameter>(param: TypeParameter): TypeParameter {
// function body
}
You can name your type parameters anything, but common conventions include:
-
T
for "Type" (most common) -
K
for "Key" andV
for "Value" (in key-value scenarios) -
U
,V
,W
for additional type parameters
Real-World Examples
1. API Response Handler
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
// Now we can handle different types of API responses
const userResponse: ApiResponse<User> = {
data: { id: 1, name: 'Alice' },
status: 200,
message: 'Success'
};
const productsResponse: ApiResponse<Product[]> = {
data: [{ id: 1, name: 'Laptop' }, { id: 2, name: 'Phone' }],
status: 200,
message: 'Success'
};
2. Generic Promise Wrapper
async function fetchData<T>(url: string): Promise<T> {
const response = await fetch(url);
return response.json();
}
// TypeScript knows exactly what type we're getting back
const user = await fetchData<User>('/api/users/1');
const posts = await fetchData<Post[]>('/api/posts');
3. Type-Safe Storage System
class Storage<T> {
private items: T[] = [];
add(item: T): void {
this.items.push(item);
}
get(index: number): T | undefined {
return this.items[index];
}
getAll(): T[] {
return [...this.items];
}
}
// Different storage instances for different types
const userStorage = new Storage<User>();
const productStorage = new Storage<Product>();
userStorage.add({ id: 1, name: 'Alice' }); // ✅ Works
userStorage.add('invalid'); // ❌ TypeScript error!
Advanced Generic Patterns
Generic Constraints: Setting Boundaries
Sometimes you want your generic to work with any type, but that type must have certain properties:
interface Identifiable {
id: number;
}
function updateItem<T extends Identifiable>(item: T, updates: Partial<T>): T {
return { ...item, ...updates };
}
// This works because User has an id property
interface User extends Identifiable {
name: string;
email: string;
}
const user: User = { id: 1, name: 'Alice', email: 'alice@example.com' };
const updatedUser = updateItem(user, { name: 'Alice Smith' });
Multiple Type Parameters
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
const personalInfo = { name: 'Alice', age: 30 };
const workInfo = { company: 'TechCorp', position: 'Developer' };
const fullProfile = merge(personalInfo, workInfo);
// Type: { name: string; age: number; company: string; position: string; }
Conditional Types (The Mind-Bender)
type ApiResult<T> = T extends string
? { message: T }
: T extends number
? { count: T }
: { data: T };
// TypeScript automatically infers the right shape
const stringResult: ApiResult<string> = { message: "Hello" };
const numberResult: ApiResult<number> = { count: 42 };
const objectResult: ApiResult<User> = { data: { id: 1, name: "Alice" } };
Utility Types: Generics in Action
TypeScript comes with powerful built-in generic utility types:
interface User {
id: number;
name: string;
email: string;
password: string;
}
// Pick only specific properties
type PublicUser = Pick<User, 'id' | 'name'>;
// Make all properties optional
type PartialUser = Partial<User>;
// Exclude specific properties
type UserWithoutPassword = Omit<User, 'password'>;
// Create a record type
type UserRoles = Record<string, 'admin' | 'user' | 'guest'>;
Common Pitfalls and How to Avoid Them
1. The Generic Type Erasure Trap
// ❌ This won't work as expected
function createArray<T>(length: number, value: T): T[] {
return new Array(length).fill(value);
}
// ✅ Better approach
function createArray<T>(length: number, factory: () => T): T[] {
return Array.from({ length }, factory);
}
2. Forgetting About Type Inference
// ❌ Unnecessary explicit type
const numbers = identity<number[]>([1, 2, 3]);
// ✅ Let TypeScript infer
const numbers = identity([1, 2, 3]); // Type is automatically number[]
When NOT to Use Generics
Generics are powerful, but they're not always the answer:
- Simple, one-off functions that will never need to handle multiple types
- When the type is always the same – just use the specific type
- Over-abstraction – if your generic has more than 3-4 type parameters, reconsider your design
Wrapping Up: Your Generic Journey Starts Here
Generics might seem intimidating at first, but they're one of TypeScript's most powerful features. They let you write flexible, reusable code without sacrificing type safety – like having your cake and eating it too! 🍰
Start small: convert a few of your existing functions to use generics. You'll quickly see how they can eliminate code duplication and make your APIs more flexible. Before you know it, you'll be thinking in generics and wondering how you ever lived without them.
Remember, the best way to learn generics is by using them. So fire up your editor, pick a function that handles multiple types, and give it the generic treatment. Your future self (and your teammates) will thank you!
Top comments (0)