How to properly type HTML elements in React
TL;DR
In my opinion, #1 is the best approach for typing React components, gives type safety on all native HTML attributes.
I'll list here different apporaches in typescript.
1. Using Interfaces with ComponentProps
import { ComponentProps } from 'react';
interface ButtonProps extends ComponentProps<'button'> {
className: string
children: React.ReactNode
}
function Button({ className, children, ...props}: ButtonProps) {
return <button className={className} {...props}>{children}</>;
}
// or
const Button = ({ className, children, ...props }: ButtonProps) => {
return <button className={className} {...props}>{children}</>;
};
// or
const Button: FC<ButtonProps>= ({ className, children, ...props }) => {
return <button className={className} {...props}>{children}</>;
};The advantage of a well-typed component is that you can pass to <Button> component all props that the <button> element could take plus className.
<Button
className="x-button"
// e is properly typed
onClick={(e) => {}} // 'e: React.MouseEvent<HTMLButtonElement, MouseEvent>'
>
Click Here
</>2. Using Interface/Type Aliases
There is a overhead using type instead of interface explained in this article. The article states that interface extends makes your ts-server perfomance slighter faster than & from type.
Faster TS perfomance:
import { ButtonHTMLAttributes } from 'react';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
className: string
};
const Button = ({ className, ...props }: ButtonProps) => {
return <button className={className} {...props} />;
};Slower TS perfomance:
import { ButtonHTMLAttributes } from 'react';
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
className: string
};
const Button = ({ className, ...props }: ButtonProps) => {
return <button className={className} {...props} />;
};3. Using Generic Components
Generic Component is useful when you want to render different HTML element (e.g., button, a, div) under <Button> component, a.k.a polymorphic component.
import { ComponentProps } from 'react';
type GenericButtonProps<T extends React.ElementType = 'button'> = {
asElement?: T;
children: ReactNode;
} & Omit<ComponentProps<T>, 'asElement'>; // include all props for the chosen element/component, except 'as' (to avoid duplication).
const Button = <T extends React.ElementType = 'button'>({
asElement,
children,
...props
}: GenericButtonProps<T>) => {
const Component = asElement || 'button';
return <Component {...props}>{children}</Component>;
};Usage:
// As a button
<Button onClick={() => alert('Clicked!')}>Click Me</Button>
// As an anchor tag
<Button as="a" href="/about">Go to About</Button>
// As a custom component (e.g., Next.js Link)
import Link from 'next/link';
<Button as={Link} href="/contact">Contact Us</Button>4. Inline Type Definition
const Button = ({
isLoading = false,
...props
}: {
isLoading?: boolean;
} & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
return <button {...props} disabled={isLoading} />;
};Best Approach
The best approach for typing React components is using interfaces that extend ComponentProps from React, as shown in the first example.
Try using interface over type.
For complex polymorphic components that can render as different HTML elements, choose the generic approach (#3).