Prop Specificity
Replacing vague primitives with union types, template literals, and accessible text props.
Variant type definition
enum ButtonVariant {
Primary = 'primary',
Secondary = 'secondary',
Danger = 'danger',
}
interface ButtonProps {
label: string;
variant: ButtonVariant;
}interface ButtonProps {
label: string;
variant: 'primary' | 'secondary' | 'danger';
}String unions are preferred over enums in modern TypeScript. They're simpler, don't generate runtime code, and work naturally with string literals at call sites: variant="primary" instead of variant={ButtonVariant.Primary}.
Specific collection prop names
interface UserListProps {
data: User[];
selected: string;
onClick: (item: User) => void;
}interface UserListProps {
users: User[];
selectedUserId: string;
onUserSelect: (user: User) => void;
}Every prop is specific: users (not generic data), selectedUserId (not ambiguous selected), and onUserSelect (specifies what event occurred). Specific names eliminate guesswork.
Autocomplete prop naming
interface AutocompleteProps {
/** Available items. */
items: string[];
/** Current input value. */
value: string;
/** Called on value change. */
onChange: (v: string) => void;
/** Custom render function. */
render: (item: string) => React.ReactNode;
/** Shown when there are no results. */
noResults: React.ReactNode;
}interface AutocompleteProps {
/** Available suggestion items. */
items: string[];
/** The current input value. */
value: string;
/** Called when the input value changes. */
onValueChange: (value: string) => void;
/** Custom render function for each suggestion item. */
renderItem: (item: string) => React.ReactNode;
/** Content displayed when no items match. */
emptyContent: React.ReactNode;
}Every prop is specific: renderItem (not vague render), emptyContent (not the double-negative noResults), onValueChange (not generic onChange), and parameter names spell out their meaning (value not v).
Color prop typing
interface AvatarProps {
name: string;
color: string;
}interface AvatarProps {
name: string;
backgroundColor: `#${string}` | 'currentColor';
}Two improvements: backgroundColor specifies which color (not text, not border), and the template literal type narrows valid inputs to hex strings at compile time. Even just the rename from color to backgroundColor is a big clarity win.
Size prop typing
interface IconProps {
name: string;
size: number;
}interface IconProps {
name: string;
size: 'sm' | 'md' | 'lg' | 'xl';
}Union types for size are predictable and discoverable in IDE autocomplete. A raw number could be anything — pixels? rem? percentage?
Accessible text prop conventions
interface AutocompleteProps<T> {
options: T[];
value: T | null;
onChange: (value: T | null) => void;
placeholder?: string;
/** @default "Clear all" */
clearLabel?: string;
/** @default "Toggle menu" */
toggleLabel?: string;
/** @default "Nothing found" */
emptyMessage?: string;
/** @default "Please wait..." */
pendingMessage?: string;
}interface AutocompleteProps<T> {
options: T[];
value: T | null;
onChange: (value: T | null) => void;
placeholder?: string;
/** Label for the clear button. @default "Clear" */
clearText?: string;
/** Label for the open popup button. @default "Open" */
openText?: string;
/** Label for the close popup button. @default "Close" */
closeText?: string;
/** Text shown when no options match. @default "No options" */
noOptionsText?: React.ReactNode;
/** Text shown while loading. @default "Loading…" */
loadingText?: React.ReactNode;
}Consistent *Text suffixes make the API predictable. Separate openText/closeText props (not a single toggleLabel) match the actual UI states. React.ReactNode for displayed messages allows rich formatting.
MUI's Autocomplete uses this exact pattern — every interactive element and user-facing string gets a *Text prop with a sensible default.
Complete component API
interface TableColumn {
/** Column identifier for sorting. */
name: string;
/** Display header text. */
label: string;
}
interface TableProps {
/** Row data. */
data: object[];
/** Column definitions. */
columns: TableColumn[];
/** Enable row selection. */
isSelectable?: boolean;
/** Called when selection changes. */
onSelect?: Function;
/** Current page number. */
page?: number;
/** Called when page changes. */
onPageChange?: Function;
/** Active filter query. */
filter?: string;
/** Called when filter changes. */
onFilterChange?: Function;
/** Whether data is loading. */
isLoading?: boolean;
}interface TableColumn<T> {
/** Unique key matching a property of T. */
key: keyof T;
/** Display header label. */
label: string;
/** Whether this column is sortable. @default false */
isSortable?: boolean;
}
interface TableProps<T extends Record<string, unknown>> {
/** The row data to display. */
data: T[];
/** Column configuration objects. */
columns: TableColumn<T>[];
/** Called when selected rows change. */
onSelectionChange?: (rows: T[]) => void;
/** Called when the page changes. */
onPageChange?: (page: number) => void;
/** Called when the filter query changes. */
onFilterChange?: (query: string) => void;
/** Whether data is currently being fetched. */
isLoading?: boolean;
}Generics make columns and callbacks type-safe: key: keyof T ensures column keys match the data shape, and callbacks receive typed values instead of Function.
isSortable lives on each column (not the whole table), and selection/pagination use typed event callbacks — not a mix of state props and untyped setters.