Back to Tutorial Overview
Master TypeScript Generics
Learn to write flexible, reusable, and type-safe code with generics
Section 1: Why Generics?
The Problem Without Generics
Imagine you need a function that returns the first element of an array. Without generics:
// Option 1: Lose type safety
function getFirst(arr: any[]): any {
return arr[0];
}
const num = getFirst([1, 2, 3]); // Type is 'any' â
const str = getFirst(['a', 'b']); // Type is 'any' â
// Option 2: Create multiple functions
function getFirstNumber(arr: number[]): number {
return arr[0];
}
function getFirstString(arr: string[]): string {
return arr[0];
}
// ... need a function for every type! đĢThe Solution: Generics
Generics allow us to create ONE function that works with ANY type while maintaining type safety:
// Generic function - works with any type!
function getFirst<T>(arr: T[]): T {
return arr[0];
}
const num = getFirst([1, 2, 3]); // Type is 'number' â
const str = getFirst(['a', 'b']); // Type is 'string' â
const bool = getFirst([true, false]); // Type is 'boolean' â
// TypeScript infers the type automatically!Section 2: Generic Functions
Basic Generic Function
// Identity function - returns the same type it receives
function identity<T>(value: T): T {
return value;
}
// Usage - TypeScript infers the type
const numResult = identity(42); // number
const strResult = identity("hello"); // string
const objResult = identity({ x: 10 }); // { x: number }
// You can also explicitly specify the type
const explicitResult = identity<string>("world");Multiple Type Parameters
// Function with two generic types
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const numStr = pair(1, "hello"); // [number, string]
const boolObj = pair(true, { x: 5 }); // [boolean, { x: number }]
// Create a map from two arrays
function createMap<K, V>(keys: K[], values: V[]): Map<K, V> {
const map = new Map<K, V>();
keys.forEach((key, index) => {
map.set(key, values[index]);
});
return map;
}
const userMap = createMap(
['user1', 'user2'],
[{ age: 25 }, { age: 30 }]
);
// Map<string, { age: number }>Generic Arrow Functions
// Arrow function syntax
const wrap = <T>(value: T): { value: T } => {
return { value };
};
// Array methods with generics
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map<number>(n => n * 2);
const strings = numbers.map<string>(n => n.toString());
// Filter with type narrowing
const items = [1, "hello", 2, "world", 3];
const numbersOnly = items.filter((item): item is number => {
return typeof item === 'number';
}); // Type is number[]Section 3: Generic Interfaces & Type Aliases
Generic Interfaces
// Generic API response interface
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
// Usage with different data types
type UserResponse = ApiResponse<{ id: string; name: string }>;
type PostsResponse = ApiResponse<Array<{ title: string; content: string }>>;
// Generic key-value store
interface KeyValueStore<K, V> {
get(key: K): V | undefined;
set(key: K, value: V): void;
has(key: K): boolean;
delete(key: K): boolean;
}
// Implementation
class SimpleStore<K, V> implements KeyValueStore<K, V> {
private store = new Map<K, V>();
get(key: K): V | undefined {
return this.store.get(key);
}
set(key: K, value: V): void {
this.store.set(key, value);
}
has(key: K): boolean {
return this.store.has(key);
}
delete(key: K): boolean {
return this.store.delete(key);
}
}
const userStore = new SimpleStore<string, User>();
const configStore = new SimpleStore<string, Config>();Generic Type Aliases
// Generic promise type
type AsyncResult<T> = Promise<T | Error>;
// Generic callback type
type Callback<T> = (data: T) => void;
// Generic factory function type
type Factory<T> = () => T;
// Usage
const fetchUser: AsyncResult<User> = fetchUserData();
const handleData: Callback<string> = (data) => console.log(data);
const createUser: Factory<User> = () => ({ id: '1', name: 'John' });Section 4: Generic Classes
Building a Generic Stack
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
isEmpty(): boolean {
return this.items.length === 0;
}
size(): number {
return this.items.length;
}
clear(): void {
this.items = [];
}
}
// Usage with different types
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
console.log(numberStack.pop()); // 2
const stringStack = new Stack<string>();
stringStack.push("hello");
stringStack.push("world");Generic Queue with Advanced Features
class Queue<T> {
private items: T[] = [];
enqueue(item: T): void {
this.items.push(item);
}
dequeue(): T | undefined {
return this.items.shift();
}
front(): T | undefined {
return this.items[0];
}
isEmpty(): boolean {
return this.items.length === 0;
}
size(): number {
return this.items.length;
}
// Generic method within generic class
map<U>(fn: (item: T) => U): Queue<U> {
const newQueue = new Queue<U>();
this.items.forEach(item => {
newQueue.enqueue(fn(item));
});
return newQueue;
}
filter(predicate: (item: T) => boolean): Queue<T> {
const newQueue = new Queue<T>();
this.items.filter(predicate).forEach(item => {
newQueue.enqueue(item);
});
return newQueue;
}
}
// Usage
const taskQueue = new Queue<{ id: number; name: string }>();
taskQueue.enqueue({ id: 1, name: "Task 1" });
taskQueue.enqueue({ id: 2, name: "Task 2" });
// Transform queue items
const idQueue = taskQueue.map(task => task.id); // Queue<number>Section 5: Generic Constraints
Constraining with extends
Sometimes you need to ensure a generic type has certain properties:
// Constraint: T must have a length property
function logLength<T extends { length: number }>(item: T): void {
console.log(`Length: ${item.length}`);
}
logLength("hello"); // â
string has length
logLength([1, 2, 3]); // â
array has length
logLength({ length: 5 }); // â
object with length
// logLength(123); // â Error: number doesn't have length
// Constraint: T must extend a base interface
interface Identifiable {
id: string;
}
function findById<T extends Identifiable>(
items: T[],
id: string
): T | undefined {
return items.find(item => item.id === id);
}
interface User extends Identifiable {
name: string;
}
interface Product extends Identifiable {
price: number;
}
const users: User[] = [{ id: '1', name: 'John' }];
const foundUser = findById(users, '1'); // Type is User | undefinedUsing keyof Constraint
// Get property value safely
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = {
id: '1',
name: 'John',
age: 30
};
const name = getProperty(user, 'name'); // Type is string
const age = getProperty(user, 'age'); // Type is number
// const x = getProperty(user, 'invalid'); // â Error
// Update property safely
function setProperty<T, K extends keyof T>(
obj: T,
key: K,
value: T[K]
): void {
obj[key] = value;
}
setProperty(user, 'age', 31); // â
// setProperty(user, 'age', 'invalid'); // â Error: wrong typeSection 6: Advanced Generic Patterns
Conditional Types
// Conditional type: unwrap Promise type
type Awaited<T> = T extends Promise<infer U> ? U : T;
type A = Awaited<Promise<string>>; // string
type B = Awaited<number>; // number
// Exclude null and undefined
type NonNullable<T> = T extends null | undefined ? never : T;
type C = NonNullable<string | null>; // string
type D = NonNullable<number | undefined>; // number
// Extract function return type
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() {
return { id: '1', name: 'John' };
}
type UserType = ReturnType<typeof getUser>; // { id: string; name: string }Mapped Types
// Make all properties optional
type Partial<T> = {
[K in keyof T]?: T[K];
};
// Make all properties required
type Required<T> = {
[K in keyof T]-?: T[K];
};
// Make all properties readonly
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
// Pick specific properties
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
// Usage
interface User {
id: string;
name: string;
email: string;
age?: number;
}
type PartialUser = Partial<User>;
// { id?: string; name?: string; email?: string; age?: number }
type UserPreview = Pick<User, 'id' | 'name'>;
// { id: string; name: string }Real-World Example: API Client
Type-Safe API Client with Generics
class ApiClient {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
async get<T>(endpoint: string): Promise<T> {
const response = await fetch(`${this.baseUrl}${endpoint}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
async post<T, U>(endpoint: string, data: T): Promise<U> {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
}
// Usage
interface User {
id: string;
name: string;
email: string;
}
interface CreateUserDto {
name: string;
email: string;
}
const api = new ApiClient('https://api.example.com');
// Type-safe API calls
const users = await api.get<User[]>('/users');
const newUser = await api.post<CreateUserDto, User>('/users', {
name: 'John',
email: 'john@example.com'
});
// TypeScript knows the exact types! â¨Practice Makes Perfect!
Start using generics in your projects to master these concepts.
Back to Tutorial Overview