Chapter 4 of 4
Generic Components and Practices
Components that adapt to their data, and the habits worth keeping.
A component that works with any item type - a list, a table, a select - should be generic rather than typed with any.
interface ListProps<T> {
items: T[];
getKey: (item: T) => string;
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, getKey, renderItem }: ListProps<T>) {
return (
<ul>
{items.map((item) => (
<li key={getKey(item)}>{renderItem(item)}</li>
))}
</ul>
);
}
// T is inferred as User at the call site
<List
items={users}
getKey={(user) => user.id}
renderItem={(user) => <span>{user.name}</span>}
/>Useful utility types
type UserProps = React.ComponentProps<typeof UserCard>; // borrow a component's props
type PartialUser = Partial<User>; // all fields optional
type UserPreview = Pick<User, "id" | "name">; // a subset
type WithoutId = Omit<User, "id">; // everything but id
type Status = "idle" | "loading" | "error"; // a closed setPractices worth adopting
- Turn on
strictin tsconfig. Half the value of TypeScript is in the strict null checks. - Prefer union types over booleans for state:
"idle" | "loading" | "error"makes invalid combinations unrepresentable. - Avoid
any. Useunknownwhen a type is genuinely unknown and narrow it before use. - Do not annotate what is already inferred - extra annotations go stale.
- Type the boundaries, not the middle: API responses and component props matter most.
// Booleans allow impossible states
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false); // both true? both false?
// A union does not
const [status, setStatus] = useState<"idle" | "loading" | "error">("idle");Model status as a closed set
Complete nextStatus so it returns 'loading' from 'idle', 'idle' from 'loading', and 'idle' from 'error'.