Write React components with TypeScript using discriminated unions, generic props, and forwardRef. Use when typing component props, creating generic reusable components, forwarding refs with proper types, or fixing TS errors in React code. Do not use for Next.js-specific patterns (prefer nextjs-app-router) or state management architecture (prefer state-management).
Write type-safe React components using TypeScript: discriminated union props, generic components, forwardRef, and proper event typing.
<List<T>>, <Select<T>>)forwardRef and proper generic typingnextjs-app-routerstate-managementforms-validationtypeinterfacetype Props = { variant: 'primary'; icon: ReactNode } | { variant: 'ghost' }.function List<T>({ items, renderItem }: { items: T[]; renderItem: (item: T) => ReactNode }).forwardRef<HTMLDivElement, Props>(). Add displayName for DevTools.onClick: (e: React.MouseEvent<HTMLButtonElement>) => void. Use React.ChangeEvent<HTMLInputElement> for inputs.useState<User | null>(null), useRef<HTMLDivElement>(null). Avoid any.ComponentPropsWithoutRef — extend native element props: type Props = ComponentPropsWithoutRef<'button'> & { variant: string }.types.ts. Co-locate component-specific types with the component.type ButtonProps =
| { variant: 'primary'; loading?: boolean; onClick: () => void }
| { variant: 'link'; href: string }
| { variant: 'icon'; icon: ReactNode; 'aria-label': string; onClick: () => void };
function Button(props: ButtonProps) {
switch (props.variant) {
case 'primary':
return <button onClick={props.onClick} disabled={props.loading}>Submit</button>;
case 'link':
return <a href={props.href}>Link</a>;
case 'icon':
return <button onClick={props.onClick} aria-label={props['aria-label']}>{props.icon}</button>;
}
}
type ListProps<T> = {
items: T[];
renderItem: (item: T) => ReactNode;
keyExtractor: (item: T) => string;
};
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return <ul>{items.map(item => <li key={keyExtractor(item)}>{renderItem(item)}</li>)}</ul>;
}
// Usage — T is inferred
<List items={users} renderItem={u => <span>{u.name}</span>} keyExtractor={u => u.id} />
type InputProps = ComponentPropsWithoutRef<'input'> & { label: string };
const Input = forwardRef<HTMLInputElement, InputProps>(({ label, ...props }, ref) => (
<div>
<label>{label}</label>
<input ref={ref} {...props} />
</div>
));
Input.displayName = 'Input';
type over interface for props — unions require type; consistency matters.React.FC — it adds implicit children and breaks generics.variant: 'loading' not isLoading?: boolean.ComponentPropsWithoutRef to extend native props — avoids ref conflicts.children explicit — { children: ReactNode } not implicit.nextjs-app-router — Next.js-specific patternsstate-management — typed state managementforms-validation — typed form patterns