Generics in TypeScript
TypeScript generics are like flexible templates that let you write reusable code while keeping full type safety. Instead of locking your functions and classes to specific types, generics allow them to work with different types while still catching errors during development. This powerful feature helps you build more adaptable and maintainable code.
Understanding Generics and Why They Matter
Imagine you're writing a function that returns whatever input it receives. Without generics, you'd have to write separate functions for strings, numbers, and other types - or use any
, which loses type safety. Generics solve this by letting you create a "type placeholder" that gets filled in when the code is used.
Here's a simple example showing the problem and how generics help:
// Without generics (limited or unsafe)
function identityAny(arg: any): any {
return arg; // We lose type information
}
// With generics (type-safe and flexible)
function identity<T>(arg: T): T {
return arg; // Keeps the exact type
}
// Usage
let output1 = identity<string>("hello"); // Type: string
let output2 = identity<number>(42); // Type: number
let output3 = identity("TypeScript"); // Type inferred as string
Key benefits of generics:
- Reusability: Write one function that works with multiple types
- Type Safety: Maintain proper type checking throughout
- Intellisense: Get full code completion in your editor
- Avoid Any: Prevent the pitfalls of using
any
type
Writing Generic Functions and Classes
Generics work with both functions and classes, following similar principles. The angle brackets (<>
) declare type parameters that get filled in when the code is used.
Generic Functions
You've seen a basic generic function. Let's explore more practical examples:
// Generic function working with arrays
function getFirstElement<T>(arr: T[]): T {
return arr[0];
}
const numbers = [1, 2, 3];
const firstNum = getFirstElement(numbers); // Type: number
const strings = ["a", "b", "c"];
const firstStr = getFirstElement(strings); // Type: string
Generic Classes
Classes can also use generics to create flexible data structures:
class Box<T> {
private content: T;
constructor(value: T) {
this.content = value;
}
getValue(): T {
return this.content;
}
}
// Usage
const stringBox = new Box("secret message");
console.log(stringBox.getValue()); // Type: string
const numberBox = new Box(100);
console.log(numberBox.getValue()); // Type: number
Multiple Type Parameters
Functions and classes can accept multiple generic types:
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const mixedPair = pair("hello", 42); // Type: [string, number]
Using Generics with Constraints
Sometimes you need to ensure generic types meet certain requirements. TypeScript lets you add constraints using the extends
keyword.
Basic Constraints
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(arg: T): void {
console.log(arg.length);
}
logLength("hello"); // Works (strings have length)
logLength([1, 2, 3]); // Works (arrays have length)
// logLength(42); // Error: number doesn't have length
Using Type Parameters in Constraints
You can constrain one type parameter based on another:
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
const person = { name: "Alice", age: 30 };
getProperty(person, "name"); // Works
// getProperty(person, "email"); // Error: "email" doesn't exist
Default Type Parameters
Generics can have default types when none are provided:
function createArray<T = string>(length: number, value: T): T[] {
return Array(length).fill(value);
}
const strArray = createArray(3, "x"); // string[]
const numArray = createArray<number>(3, 0); // number[]
Real-World Use Cases
Generics shine in many practical scenarios:
1. API Response Handling
interface ApiResponse<T> {
data: T;
status: number;
}
function fetchUser(): Promise<ApiResponse<User>> {
// API call implementation
}
2. State Management
class State<T> {
constructor(private current: T) {}
update(newValue: T) {
this.current = newValue;
}
get(): T {
return this.current;
}
}
3. Higher-Order Functions
function timedFunction<T extends (...args: any[]) => any>(fn: T): T {
return function(...args: Parameters<T>): ReturnType<T> {
console.time("Function timer");
const result = fn(...args);
console.timeEnd("Function timer");
return result;
} as T;
}
By mastering generics, you'll write TypeScript code that's both flexible and type-safe, reducing duplication while maintaining excellent code quality. The key is to start simple and gradually incorporate more advanced generic patterns as you become comfortable with the core concepts.