In this quick tip, excerpted from Unleashing the Power of TypeScript, Steve shows you how to extend the properties of an HTML element in TypeScript.
In most of the larger applications and projects I’ve worked on, I often find myself building a bunch of components that are really supersets or abstractions on top of the standard HTML elements. Some examples include custom button elements that might take a prop defining whether or not that button should be a primary or secondary button, or maybe one that indicates that it will invoke a dangerous action, such as deleting or removing a item from the database. I still want my button to have all the properties of a button in addition to the props I want to add to it.
Another common case is that I’ll end up creating a component that allows me to define a label and an input field at once. I don’t want to re-add all of the properties that an element takes. I want my custom component to behave just like an input field, but also take a string for the label and automatically wire up the htmlFor prop on the to correspond with the id on the .
In JavaScript, I can just use {...props} to pass through any props to an underlying HTML element. This can be a bit trickier in TypeScript, where I need to explicitly define what props a component will accept. While it’s nice to have fine-grained control over the exact types that my component accepts, it can be tedious to have to add in type information for every single prop manually.
In certain scenarios, I need a single adaptable component, like a
, that changes styles according to the current theme. For example, maybe I want to define what styles should be used depending on whether or not the user has manually enabled light or dark mode for the UI. I don’t want to redefine this component for every single block element (such as , , , and so on). It should be capable of representing different semantic HTML elements, with TypeScript automatically adjusting to these changes.
There are a couple of strategies that we can employ:
For components where we’re creating an abstraction over just one kind of element, we can extend the properties of that element.
For components where we want to define different elements, we can create polymorphic components. A polymorphic component is a component designed to render as different HTML elements or components while maintaining the same properties and behaviors. It allows us to specify a prop to determine its rendered element type. Polymorphic components offer flexibility and reusability without us having to reimplement the component. For a concrete example, you can look at Radix’s implementation of a polymorphic component.
In this tutorial, we’ll look at the first strategy.
Mirroring and Extending the Properties of an HTML Element
Let’s start with that first example mentioned in the introduction. We want to create a button that comes baked in with the appropriate styling for use in our application. In JavaScript, we might be able to do something like this:
const Button = (props) => {
return ;
};
In TypeScript, we could just add what we know we need. For example, we know that we need the children if we want our custom button to behave the same way an HTML button does:
You can imagine that adding properties one at a time could get a bit tedious. Instead, we can tell TypeScript that we want to match the same props that it would use for a element in React:
But we have a new problem. Or, rather, we had a problem that also existed in the JavaScript example and which we ignored. If someone using our new Button component passes in a className prop, it will override our className. We could (and we will) add some code to deal with this in a moment, but I don’t want to pass up the opportunity to show you how to use a utility type in TypeScript to say “I want to use all of the props from an HTML button except for one (or more)”:
Now, TypeScript will stop us or anyone else from passing a className property into our Button component. If we just wanted to extend the class list with whatever is passed in, we could do that in a few different ways. We could just append it to the list:
We’re now saying that Button accepts all of the props that a element accepts plus one more: variant. This prop will show up with all the other props we inherited from HTMLButtonElement.
We can add support to our Button to add this class as well:
Another common component that I typically end up making for myself is a component that correctly wires up a label and input element with the correct for and id attributes respectively. I tend to grow weary typing this out over and over:
Without extending the props of an HTML element, I might end up slowly adding props as needed:
As we saw with the button, we can refactor it in a similar fashion:
type LabeledInputProps = React.ComponentProps<'input'> & {
label: string;
};
Other than label, which we’re passing to the (uhh) label that we’ll often want grouped with our inputs, we’re manually passing props through one by one. Do we want to add autofocus? Better add another prop. It would be better to do something like this:
We can pull out the things we need to work with and then just pass everything else on through to the component, and then just pretend for the rest of our days that it’s a standard HTMLInputElement.
TypeScript doesn’t care, since HTMLElement is pretty flexible, as the DOM pre-dates TypeScript. It only complains if we toss something completely egregious in there.