Purpose
Write type-safe React components using TypeScript: discriminated union props, generic components, forwardRef, and proper event typing.
When to use this skill
- typing component props with discriminated unions for variant patterns
- creating generic reusable components (
<List<T>>,<Select<T>>) - forwarding refs with
forwardRefand proper generic typing - fixing TypeScript errors in React components or hooks
Do not use this skill when
- working with Next.js App Router specifics — prefer
nextjs-app-router - designing state architecture — prefer
state-management - building forms — prefer
forms-validation
Procedure
- Define props as types — use
type(notinterface) for component props. Export for reuse. - Use discriminated unions — for variant props:
type Props = { variant: 'primary'; icon: ReactNode } | { variant: 'ghost' }. - Make generic components —
function List<T>({ items, renderItem }: { items: T[]; renderItem: (item: T) => ReactNode }). - Forward refs correctly — use
forwardRef<HTMLDivElement, Props>(). AdddisplayNamefor DevTools. - Type events —
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void. UseReact.ChangeEvent<HTMLInputElement>for inputs. - Type hooks —
useState<User | null>(null),useRef<HTMLDivElement>(null). Avoidany. - Use
ComponentPropsWithoutRef— extend native element props:type Props = ComponentPropsWithoutRef<'button'> & { variant: string }. - Extract shared types — put reusable types in
types.ts. Co-locate component-specific types with the component.
Discriminated unions
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>;
}
}
Generic components
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} />
forwardRef pattern
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';
Decision rules
typeoverinterfacefor props — unions requiretype; consistency matters.- Never use
React.FC— it adds implicitchildrenand breaks generics. - Discriminated unions over optional booleans —
variant: 'loading'notisLoading?: boolean. ComponentPropsWithoutRefto extend native props — avoids ref conflicts.- Keep
childrenexplicit —{ children: ReactNode }not implicit.
References
Related skills
nextjs-app-router— Next.js-specific patternsstate-management— typed state managementforms-validation— typed form patterns
