Imagine a universal navigation system on the spaceship that can display data about planets, stars, ships - whatever you feed it. In TypeScript, we create such "universal" components using generics.
Generics are type parameters - they allow you to create components and functions that work with different data types while maintaining full type control:
1// Without generics - you need separate functions
2function getFirstPlanet(items: Planet[]): Planet {
3 return items[0];
4}
5function getFirstStar(items: Star[]): Star {
6 return items[0];
7}
8
9// With generics - one function for all types
10function getFirst<T>(items: T[]): T {
11 return items[0];
12}
13
14const planet = getFirst<Planet>(planets); // returns Planet
15const star = getFirst<Star>(stars); // returns Star
16const name = getFirst<string>(["a", "b"]); // returns stringOne of the most common uses of generics in React is a universal list component:
1// Each element must have an id for display
2interface HasId {
3 id: number | string;
4}
5
6interface ListProps<T extends HasId> {
7 items: T[];
8 renderItem: (item: T) => React.ReactNode;
9 emptyMessage?: string;
10}
11
12function List<T extends HasId>({ items, renderItem, emptyMessage = "No data" }: ListProps<T>) {
13 if (items.length === 0) {
14 return <p>{emptyMessage}</p>;
15 }
16
17 return (
18 <ul>
19 {items.map(item => (
20 <li key={item.id}>{renderItem(item)}</li>
21 ))}
22 </ul>
23 );
24}1interface Planet {
2 id: number;
3 name: string;
4 type: string;
5}
6
7interface CrewMember {
8 id: string;
9 name: string;
10 rank: string;
11}
12
13function Dashboard() {
14 const planets: Planet[] = [
15 { id: 1, name: "Mars", type: "rocky" },
16 { id: 2, name: "Jupiter", type: "gas giant" }
17 ];
18
19 const crew: CrewMember[] = [
20 { id: "c1", name: "Nova", rank: "Captain" },
21 { id: "c2", name: "Ra", rank: "Navigator" }
22 ];
23
24 return (
25 <div>
26 <h2>Planets</h2>
27 <List<Planet>
28 items={planets}
29 renderItem={(planet) => <span>{planet.name} ({planet.type})</span>}
30 />
31
32 <h2>Crew</h2>
33 <List<CrewMember>
34 items={crew}
35 renderItem={(member) => <span>{member.name} - {member.rank}</span>}
36 />
37 </div>
38 );
39}1interface SelectOption {
2 value: string;
3 label: string;
4}
5
6interface SelectProps<T extends SelectOption> {
7 options: T[];
8 value: string;
9 onChange: (option: T) => void;
10 placeholder?: string;
11}
12
13function Select<T extends SelectOption>({
14 options, value, onChange, placeholder
15}: SelectProps<T>) {
16 const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
17 const selected = options.find(opt => opt.value === e.target.value);
18 if (selected) onChange(selected);
19 };
20
21 return (
22 <select value={value} onChange={handleChange}>
23 {placeholder && <option value="">{placeholder}</option>}
24 {options.map(opt => (
25 <option key={opt.value} value={opt.value}>
26 {opt.label}
27 </option>
28 ))}
29 </select>
30 );
31}The
extends keyword lets you restrict which types are accepted:1// T must have a 'name' field
2function displayName<T extends { name: string }>(item: T): string {
3 return item.name;
4}
5
6// T must implement the Sortable interface
7interface Sortable {
8 sortOrder: number;
9}
10
11function sortItems<T extends Sortable>(items: T[]): T[] {
12 return [...items].sort((a, b) => a.sortOrder - b.sortOrder);
13}