Generics in TypeScript allow you to write reusable, flexible code that works with a wide variety of types while maintaining type safety. By using generics, you can define components, functions, or classes that aren't tied to a specific data type, making your codebase more robust and adaptable.
In this post, we’ll explore the power of generics, understand their syntax, and apply them through practical examples.
What Are Generics?
Generics let you create type-safe abstractions. Instead of hardcoding a specific type, you can define a placeholder (often called T) that gets replaced with the actual type during usage.
Basic Syntax:
function identity(value: T): T { return value;}
const number = identity(42);
// T is inferred as number
const text = identity("Hello, World!"); // T is inferred as string
In the example above, the function identity works with any type, determined dynamically when it’s called
Generic Functions
Generics are commonly used in functions to enforce type safety while maintaining flexibility.
Example: A Generic Array Filter:
function filterArray(arr: T[], callback: (item: T) => boolean): T[] {
return arr.filter(callback);
}
const numbers = [1, 2, 3, 4];
const evenNumbers = filterArray(numbers, (num) => num % 2 === 0);
console.log(evenNumbers); // [2, 4]
Here, the function works with arrays of any type (T[]), ensuring that the input, callback, and output remain consistent.
Generic Interfaces
Generics also shine in defining flexible interfaces.
Example: A Key-Value Store Interface:
interface KeyValueStore {
key: K;
value: V;
}
const userStore: KeyValueStore = {
key: "userId",
value: 101,
};
This pattern is especially useful for data structures like maps or dictionaries.
Generic Classes
When building reusable components, generic classes allow you to define data structures that handle a wide range of types.
Example: A Generic Stack:
class Stack {
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();
numberStack.push(1);
numberStack.push(2);
console.log(numberStack.peek()); // 2
number
Stack.pop();
const stringStack = new Stack();
stringStack.push("Hello");
console.log(stringStack.peek()); // "Hello"
Generic Constraints
Sometimes, you want to restrict the types that can be used with a generic. This is done using constraints like extends.
Example: Enforcing Object Properties:
function getProperty(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Alice" };
console.log(getProperty(user, "name")); // "Alice"
// Error: Argument of type '"age"' is not assignable to parameter of type '"id" | "name"'
console.log(getProperty(user, "age"));
The keyof T constraint ensures that key must be a property of the object T.
Generic Default Types
You can provide default types for generics to simplify usage.
Example:
interface ApiResponse {
data: T;
status: number;
}
const response: ApiResponse = {
data: { id: 1, name: "Alice" },
status: 200,
};
Here, T defaults to any if no type is specified explicitly.
Using Generics with React Components
Generics are widely used in frameworks like React for type-safe components and hooks.
Example: A Generic Dropdown Component:
import React from "react";
interface DropdownProps<T> {
options: T[];
onSelect: (value: T) => void;
}
function Dropdown<T>({ options, onSelect }: DropdownProps<T>) {
return (
<select onChange={(e) => onSelect(options[+e.target.value])}>
{options.map((option, index) => (
<option key={index} value={index}>
{String(option)}
option>
))}
select>
);
}
// Usage with strings
<Dropdown
options={["Apple", "Banana", "Cherry"]}
onSelect={(value) => console.log(value)}
/>;
// Usage with numbers
<Dropdown
options={[1, 2, 3]}
onSelect={(value) => console.log(value)}
/>;
Conclusion
Generics in TypeScript unlock powerful ways to write reusable and flexible code. They ensure type safety while allowing developers to define abstractions that adapt to varying use cases. Whether you’re working with functions, interfaces, or classes, mastering generics is an essential skill for any TypeScript developer.