Introduction to Generics
Generics allow you to create components that work with various types while maintaining type safety. They're like "templates" that you can customize.
The Problem
Imagine you want to create a function that returns the first element of an array:
typescript
// Without generics - loses type informationfunction firstElement(arr: any[]): any { return arr[0];} const num = firstElement([1, 2, 3]); // type: anyconst str = firstElement(["a", "b"]); // type: any // We lost the information that num is number and str is string!The Solution: Generics
typescript
function firstElement<T>(arr: T[]): T { return arr[0];} const num = firstElement([1, 2, 3]); // type: numberconst str = firstElement(["a", "b"]); // type: string // TypeScript correctly infers the type!The T is a type parameter - a placeholder that will be replaced by the actual type when the function is called.
Basic Syntax
typescript
// Generic functionfunction identity<T>(value: T): T { return value;} // Calling - TypeScript infers the typeidentity("text"); // T = stringidentity(42); // T = number // Or specifying explicitlyidentity<boolean>(true);Multiple Type Parameters
typescript
function pair<K, V>(key: K, value: V): [K, V] { return [key, value];} const result = pair("age", 30); // [string, number]const other = pair(1, true); // [number, boolean]Generic Interfaces
typescript
interface Box<T> { content: T;} const textBox: Box<string> = { content: "Hello!"}; const numberBox: Box<number> = { content: 42};Generic Classes
typescript
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]; }} const numberStack = new Stack<number>();numberStack.push(1);numberStack.push(2);console.log(numberStack.pop()); // 2 const stringStack = new Stack<string>();stringStack.push("a");stringStack.push("b");Constraints
Limit which types can be used with extends:
typescript
// T must have the 'length' propertyinterface HasLength { length: number;} function getLength<T extends HasLength>(item: T): number { return item.length;} getLength("hello"); // OK - string has lengthgetLength([1, 2, 3]); // OK - array has lengthgetLength({ length: 10 }); // OK - object has length getLength(42); // Error - number doesn't have lengthUsing Type Parameters in Constraints
typescript
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key];} const person = { name: "Pedro", age: 30 }; getProperty(person, "name"); // OK, returns stringgetProperty(person, "age"); // OK, returns numbergetProperty(person, "email"); // Error - 'email' doesn't existDefault Type Parameters
typescript
interface Response<T = any> { data: T; status: number;} // Uses default type (any)const response1: Response = { data: "anything", status: 200}; // Specifies the typeconst response2: Response<User> = { data: { name: "Pedro", age: 30 }, status: 200};Practical Example: API Function
typescript
async function fetchData<T>(url: string): Promise<T> { const response = await fetch(url); return response.json();} interface User { id: number; name: string;} // TypeScript knows the return typeconst user = await fetchData<User>("/api/user/1");console.log(user.name); // Autocomplete works!Conclusion
Generics are a powerful tool for creating flexible, type-safe code. With them, you can write reusable components without sacrificing the benefits of static typing.