Learn

/

Prop Specificity

Prop Specificity

7 patterns

Replacing vague primitives with union types, template literals, and accessible text props.

Variant type definition
easy
Avoid
enum ButtonVariant {
  Primary = 'primary',
  Secondary = 'secondary',
  Danger = 'danger',
}

interface ButtonProps {
  label: string;
  variant: ButtonVariant;
}

Prefer
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}.

TypeScript: Union Types
Specific collection prop names
easy
Avoid
interface UserListProps {
  data: User[];
  selected: string;
  onClick: (item: User) => void;
}

Prefer
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.

React Docs: Passing Props
Autocomplete prop naming
medium
Avoid
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;
}

Prefer
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).

React Docs: Passing Props
Color prop typing
medium
Avoid
interface AvatarProps {
  name: string;
  color: string;
}

Prefer
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.

TypeScript: Template Literal Types
Size prop typing
medium
Avoid
interface IconProps {
  name: string;
  size: number;
}

Prefer
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?

TypeScript: Union Types
Accessible text prop conventions
medium
Avoid
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;
}

Prefer
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.

MUI: Autocomplete API
Complete component API
hard
Avoid
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;
}

Prefer
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.

TypeScript: Generics