Extending HTML
Forwarding native HTML attributes with ComponentProps, Omit, and polymorphic as props.
Custom button component props
interface ButtonProps
extends React.ComponentProps<'button'> {
/** The button text. */
label: string;
/** Click handler. */
onClick?: () => void;
/** Whether the button is disabled. */
disabled?: boolean;
/** Button type. @default 'button' */
type?: 'button' | 'submit' | 'reset';
}interface ButtonProps
extends React.ComponentProps<'button'> {
/** The button's visible text content. */
label: string;
/** Visual style variant. @default 'primary' */
variant?: 'primary' | 'secondary' | 'danger';
/** Button size. @default 'md' */
size?: 'sm' | 'md' | 'lg';
}When you extend ComponentProps<'button'>, onClick, disabled, and type are already included with correct types. Only add props that the native element doesn't have — label, variant, and size are genuine additions. Re-declaring inherited props clutters the API.
Input wrapper with custom onChange
interface TextFieldProps
extends Omit<
React.ComponentProps<'input'>,
'onChange' | 'value'
> {
/** Controlled string value. */
value: string;
/** Called on input change with the full event. */
onChange: (
event: React.ChangeEvent<HTMLInputElement>
) => void;
}interface TextFieldProps
extends Omit<
React.ComponentProps<'input'>,
'onChange'
> {
/**
* Called with the new string value on change.
* Simplified from the native event-based onChange.
*/
onChange: (value: string) => void;
}Omit should only remove props you actually need to redefine. The native value on <input> already works fine — omitting and re-declaring it as string is unnecessary.
The real win is simplifying onChange to pass just the string value, so consumers write onChange={setValue} instead of onChange={(e) => setValue(e.target.value)}.
Polymorphic component typing
type CardProps<
E extends string = 'div'
> = {
/** The HTML element to render as. */
as?: E;
children: React.ReactNode;
} & Omit<
React.HTMLAttributes<HTMLElement>,
'as' | 'children'
>;type CardProps<
E extends React.ElementType = 'div'
> = {
/** The HTML element or component to render. */
as?: E;
children: React.ReactNode;
} & Omit<
React.ComponentPropsWithoutRef<E>,
'as' | 'children'
>;React.ElementType constrains as to valid elements/components, and ComponentPropsWithoutRef<E> adapts the available props based on the element. as="a" enables href; as="button" enables type.
Compare string + HTMLAttributes<HTMLElement> which accepts any string and always gives generic div-like props regardless of the element.
Wrapper div component props
interface CardProps {
children: React.ReactNode;
className?: string;
id?: string;
style?: React.CSSProperties;
role?: string;
'aria-label'?: string;
}interface CardProps
extends React.ComponentProps<'div'> {
children: React.ReactNode;
}Extending React.ComponentProps<'div'> inherits every valid div attribute — className, style, role, all ARIA props, event handlers, and more. Only declare custom props that don't exist on the native element.
Link component extending anchor
interface LinkProps {
href: string;
children: React.ReactNode;
target?: string;
rel?: string;
className?: string;
onClick?: () => void;
}interface LinkProps
extends React.ComponentProps<'a'> {
/**
* Opens in a new tab with noopener noreferrer.
* @default false
*/
isExternal?: boolean;
}Extending <a> gives you href, target, rel, download, ARIA props, and all event handlers for free. The custom isExternal prop adds real value — it encapsulates the target="_blank" rel="noopener noreferrer" pattern into a single boolean.