How to properly type HTML elements in React

LinkIconTL;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.

LinkIcon1. 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
</>

LinkIcon2. 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} />;
};

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

LinkIcon4. Inline Type Definition

const Button = ({ 
  isLoading = false, 
  ...props 
}: { 
  isLoading?: boolean;
} & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
  return <button {...props} disabled={isLoading} />;
};

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