Using TypeScript Intersections to extend component's props in React

Every app has buttons.

So, let's create a Button component, which uses the button html element under the hood.

1
2
3
export default function Button({ children }: { children: React.ReactNode }) {
  return <button className="btn">{children}</button>
}

Easy peasy.

But in terms of functionality, our button is very limited. For example, having this implementation, we cannot handle any events, cannot extend the className, nor pass any style , disabled , etc. props to it. It's basically a very dumb component.

There are several possible approaches we can take in order to make it a useful one.

Props "babysitting" or any type

One way to do it is by "babysitting" every possible button property there is by ourselves. Like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export default function Button({
  children,
  onClick,
  style,
  disabled,
  // ... etc.
}: {
  children: React.ReactNode
  onClick: (e: MouseEvent) => void }
  style: { [key: string]: any
  disabled: boolean
  // ... etc.
}) {
  return (
    <button
      className="btn"
      onClick={onClick}
      style={style}
    >
      {children}
    </button>
  );
}

We can list all of native button's properties in our Button ' s props, or some of them, but that's very tedious and far from scalable.

Another option is to use the any type and tell TypeScript that our component doesn't care about what types are the props it receives. I'm not going into details here, because it defeats the purpose of using TypeScript in the first place. And on top of that, using any is a strong sign for being lazy - and we're not lazy. :)

So, there's a smarter approach.

Using ...rest with TypeScript's intersections.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
export default function Button({
  children,
  ...rest
}: {
  children: React.ReactNode
} & React.ComponentPropsWithoutRef<'button'>) {
  return (
    <button
      className="btn"
      {...rest}
    >
      {children}
    </button>
  );
}

Using the JavaScript's ...rest operator we collect every possible property the user of the component passes to it in an array, called rest .

After that we "spread" the array in the →button←, making our Button component act as a "proxy" to <button>.

One thing that's important to notice is the intersection type { ... } & React.ComponentPropsWithoutRef<'button'> . This is how we tell TypeScript that our Button component may receive any of the native button 's properties through the ...rest .

Sweet. We are happy. TypeScript is happy.

But we're not constrained only to extending props using only native HTML elements. We can do it with custom ones, too.

Here's an example, where we create an IconButton , which composes our Button component, but adds the ability to display an icon, next to the children . Take note that in this case we should use typeof Component to indicate that we're using a custom one.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
export default function IconButton({
  icon,
    children,
  ...rest
}: {
  icon: React.ReactNode
} & React.ComponentPropsWithoutRef<typeof Button>) {
  return (
    <Button
      {...rest}
    >
      <Icon name="add" />
      {children}
    </Button>
  );
}

Conclusion

Use React.ComponentPropsWithoutRef with intersections to compose custom component types. Forget about any and props "babysitting". Be smart and reuse whenever possible, whatever possible.

Here's very simple demo: https://codesandbox.io/s/morning-cache-3vzb4?file=/src/App.tsx

Comments

Popular posts from this blog