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