JavaScript and React course Β· Module 12: React and TypeScript
Generic Components - Universal Navigation Systems
In this lesson5
You have a planet list component. A week later you need a list of stars, then crew members, then ships. You copy the component a fourth time and change only the type - and from now on every bug has to be fixed in four places. Instead, imagine a universal navigation system on a spaceship that can display data about planets, stars, ships - anything you give it. In TypeScript, we create such "universal" components using generics.
What are Generics?
Generics are type parameters - they let you create components and functions that work with different data types while keeping full type control. You write a type parameter in angle brackets, usually a single capital letter T (for "type"). It's a slot for a type that is only supplied at the point of use - like a variable, but for types.
The example uses the Planet and Star interfaces from the props lesson, and planets and stars are arrays of such objects:
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}Both functions do exactly the same thing and differ only by type. A generic turns them into one:
1// With generics - one function for all types
2function getFirst<T>(items: T[]): T {
3 return items[0];
4}
5
6const planet = getFirst<Planet>(planets); // returns Planet
7const star = getFirst<Star>(stars); // returns Star
8const name = getFirst<string>(["a", "b"]); // returns stringRead getFirst<T>(items: T[]): T as: "for any type T, take an array of T and return one T". The type isn't lost along the way - planet has the type Planet, so the editor suggests its fields. In practice you can skip the brackets at the call site, because TypeScript infers T from the argument: getFirst(planets) gives the same result.
Generic List Component
One of the most common uses of generics in React is a universal list component. The list needs one guarantee: every element must have an id, because React requires a unique key. We write that guarantee with extends - T extends HasId means "T can be anything, as long as it has an id field". This is called a constraint.
1// Each element must have an id to use as the key
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}The renderItem prop is a so-called render prop: a function the parent passes to decide how to draw a single element. The list itself knows nothing about planets - it only knows how to lay them out:
1function List<T extends HasId>({ items, renderItem, emptyMessage = "No data" }: ListProps<T>) {
2 if (items.length === 0) {
3 return <p>{emptyMessage}</p>;
4 }
5
6 return (
7 <ul>
8 {items.map(item => (
9 <li key={item.id}>{renderItem(item)}</li>
10 ))}
11 </ul>
12 );
13}Thanks to the constraint, item.id is allowed in key. If you tried to read item.name, TypeScript would object - the list has no guarantee such a field exists. A technical note: in a .tsx file an arrow function with a generic needs <T,> with a comma, so the compiler doesn't confuse it with a JSX tag. A function declaration doesn't have this problem.
Using a Generic Component
Let's prepare two different kinds of data. Planets have a numeric id, crew members a string one, and both satisfy HasId:
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}Now the same List component displays both collections. <List<Planet> passes the type explicitly, although TypeScript would infer it from items:
1function Dashboard() {
2 const planets: Planet[] = [
3 { id: 1, name: "Mars", type: "rocky" },
4 { id: 2, name: "Jupiter", type: "gas giant" }
5 ];
6
7 const crew: CrewMember[] = [
8 { id: "c1", name: "Nova", rank: "Captain" },
9 { id: "c2", name: "Ra", rank: "Navigator" }
10 ];
11
12 return (
13 <div>
14 <h2>Planets</h2>
15 <List<Planet>
16 items={planets}
17 renderItem={(planet) => <span>{planet.name} ({planet.type})</span>}
18 />
19
20 <h2>Crew</h2>
21 <List<CrewMember>
22 items={crew}
23 renderItem={(member) => <span>{member.name} - {member.rank}</span>}
24 />
25 </div>
26 );
27}In renderItem the planet parameter has the type Planet and member the type CrewMember, with no annotations. The List component didn't change by a single line.
Generic Select Component
The same pattern works for a select field. The constraint requires value and label fields, and onChange hands the parent the whole selected object, keeping its exact type. The change handler uses ChangeEvent from the previous lesson:
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 find method can return undefined, so if (selected) is required - TypeScript won't let you pass a possible undefined to onChange.
Generic Constraints
The extends keyword lets you restrict which types are accepted. The constraint can be an inline type or a named interface:
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}A constraint is a minimum, not an exact shape: the object may have extra fields and they are kept in the result type. [...items] creates a copy, so sorting doesn't modify the original array. My advice: reach for a generic only when the same component really handles two different data types - before that, a plain interface is enough. In the final project you'll build a generic data table.
Code for this lesson: App.tsx
1import React, { useState } from 'react';
2
3// Generic constraint - every element must have an id
4interface HasId {
5 id: number | string;
6}
7
8// Generic list component
9interface ListProps<T extends HasId> {
10 items: T[];
11 renderItem: (item: T) => React.ReactNode;
12 title: string;
13 emptyMessage?: string;
14}
15
16function List<T extends HasId>({ items, renderItem, title, emptyMessage = "No items" }: ListProps<T>) {
17 return (
18 <div className="list-container">
19 <h2>{title} ({items.length})</h2>
20 {items.length === 0 ? (
21 <p className="empty">{emptyMessage}</p>
22 ) : (
23 <ul className="list">
24 {items.map(item => (
25 <li key={item.id} className="list-item">{renderItem(item)}</li>
26 ))}
27 </ul>
28 )}
29 </div>
30 );
31}
32
33// Different data types
34interface Planet {
35 id: number;
36 name: string;
37 type: string;
38 habitable: boolean;
39}
40
41interface CrewMember {
42 id: string;
43 name: string;
44 rank: string;
45 specialization: string;
46}
47
48interface Mission {
49 id: number;
50 codeName: string;
51 status: "planned" | "active" | "completed";
52 destination: string;
53}
54
55function App() {
56 const [planets] = useState<Planet[]>([
57 { id: 1, name: "Kepler-442b", type: "rocky", habitable: true },
58 { id: 2, name: "HD 209458 b", type: "gaseous", habitable: false },
59 { id: 3, name: "Proxima Centauri b", type: "rocky", habitable: true },
60 ]);
61
62 const [crew] = useState<CrewMember[]>([
63 { id: "c1", name: "Nova", rank: "Captain", specialization: "Navigation" },
64 { id: "c2", name: "Ra", rank: "Navigator", specialization: "Astrophysics" },
65 { id: "c3", name: "Astro", rank: "Engineer", specialization: "Engines" },
66 ]);
67
68 const [missions] = useState<Mission[]>([
69 { id: 1, codeName: "DAWN", status: "active", destination: "Mars" },
70 { id: 2, codeName: "HORIZON", status: "planned", destination: "Jupiter" },
71 ]);
72
73 const statusColors = { planned: "#f59e0b", active: "#22c55e", completed: "#6b7280" };
74
75 return (
76 <div className="app">
77 <h1>Generic Components</h1>
78
79 <List<Planet>
80 title="Planets"
81 items={planets}
82 renderItem={(planet) => (
83 <div className="item-content">
84 <strong>{planet.name}</strong>
85 <span className="detail">{planet.type}</span>
86 <span className={"badge " + (planet.habitable ? "green" : "red")}>
87 {planet.habitable ? "Habitable" : "Uninhabitable"}
88 </span>
89 </div>
90 )}
91 />
92
93 <List<CrewMember>
94 title="Crew"
95 items={crew}
96 renderItem={(member) => (
97 <div className="item-content">
98 <strong>{member.name}</strong>
99 <span className="detail">{member.rank}</span>
100 <span className="spec">{member.specialization}</span>
101 </div>
102 )}
103 />
104
105 <List<Mission>
106 title="Missions"
107 items={missions}
108 renderItem={(mission) => (
109 <div className="item-content">
110 <strong>{mission.codeName}</strong>
111 <span className="detail">{mission.destination}</span>
112 <span style={{ color: statusColors[mission.status] }}>
113 {mission.status.toUpperCase()}
114 </span>
115 </div>
116 )}
117 />
118 </div>
119 );
120}
121
122export default App;Remember: a generic is a universal docking port - every ship that meets the port's requirements docks without rebuilding the station.
Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What does the <T> notation mean in the declaration function getFirst<T>(items: T[]): T?
2. What does the notation <T extends { id: number }> mean in a generic React component?
Hands-on tasks in the game
- Vertical ordering
Arrange the steps for creating a generic list component in the correct order:
- Code editor
Generic Select component with constraints
- Horizontal ordering
Arrange the syntax of a generic constraint in TypeScript:
- Code editor
Generic component with render prop and filtering