On this fast tip, excerpted from Unleashing the Power of TypeScript, Steve reveals you easy methods to lengthen the properties of an HTML component in TypeScript.

In a lot of the bigger purposes and initiatives I’ve labored on, I usually discover myself constructing a bunch of elements which can be actually supersets or abstractions on prime of the usual HTML parts. Some examples embrace customized button parts that may take a prop defining whether or not or not that button must be a major or secondary button, or perhaps one which signifies that it’ll invoke a harmful motion, akin to deleting or eradicating a merchandise from the database. I nonetheless need my button to have all of the properties of a button along with the props I need to add to it.

One other frequent case is that I’ll find yourself making a part that enables me to outline a label and an enter area directly. I don’t need to re-add the entire properties that an <enter /> component takes. I need my customized part to behave identical to an enter area, however additionally take a string for the label and routinely wire up the htmlFor prop on the <label /> to correspond with the id on the <enter />.

In JavaScript, I can simply use {...props} to move via any props to an underlying HTML component. This generally is a bit trickier in TypeScript, the place I have to explicitly outline what props a part will settle for. Whereas it’s good to have fine-grained management over the precise sorts that my part accepts, it may be tedious to have so as to add in sort data for each single prop manually.

In sure situations, I would like a single adaptable part, like a <div>, that modifications kinds in keeping with the present theme. For instance, perhaps I need to outline what kinds must be used relying on whether or not or not the consumer has manually enabled gentle or darkish mode for the UI. I don’t need to redefine this part for each single block component (akin to <part>, <article>, <apart>, and so forth). It must be able to representing completely different semantic HTML parts, with TypeScript routinely adjusting to those modifications.

There are a few methods that we will make use of:

  • For elements the place we’re creating an abstraction over only one type of component, we will lengthen the properties of that component.
  • For elements the place we need to outline completely different parts, we will create polymorphic elements. A polymorphic part is a part designed to render as completely different HTML parts or elements whereas sustaining the identical properties and behaviors. It permits us to specify a prop to find out its rendered component sort. Polymorphic elements provide flexibility and reusability with out us having to reimplement the part. For a concrete instance, you’ll be able to have a look at Radix’s implementation of a polymorphic component.

On this tutorial, we’ll have a look at the primary technique.

Mirroring and Extending the Properties of an HTML Aspect

Let’s begin with that first instance talked about within the introduction. We need to create a button that comes baked in with the suitable styling to be used in our utility. In JavaScript, we would be capable of do one thing like this:

const Button = (props) => {
  return <button className="button" {...props} />;
};

In TypeScript, we might simply add what we all know we want. For instance, we all know that we want the kids if we would like our customized button to behave the identical approach an HTML button does:

const Button = ({ kids }: React.PropsWithChildren) => {
  return <button className="button">{kids}</button>;
};

You’ll be able to think about that including properties one by one might get a bit tedious. As an alternative, we will inform TypeScript that we need to match the identical props that it might use for a <button> component in React:

const Button = (props: React.ComponentProps<'button'>) => {
  return <button className="button" {...props} />;
};

However we’ve a brand new drawback. Or, slightly, we had an issue that additionally existed within the JavaScript instance and which we ignored. If somebody utilizing our new Button part passes in a className prop, it is going to override our className. We might (and we’ll) add some code to cope with this in a second, however I don’t need to move up the chance to point out you easy methods to use a utility sort in TypeScript to say β€œI need to use all of the props from an HTML button besides for one (or extra)”:

sort ButtonProps = Omit<React.ComponentProps<'button'>, 'className'>;

const Button = (props: ButtonProps) => {
  return <button className="button" {...props} />;
};

Now, TypeScript will cease us or anybody else from passing a className property into our Button part. If we simply wished to increase the category checklist with no matter is handed in, we might do this in just a few alternative ways. We might simply append it to the checklist:

sort ButtonProps = React.ComponentProps<'button'>;

const Button = (props: ButtonProps) => {
  const className = 'button ' + props.className;

  return <button className={className.trim()} {...props} />;
};

I like to make use of the clsx library when working with courses, because it takes care of most of those sorts of issues on our behalf:

import React from 'react';
import clsx from 'clsx';

sort ButtonProps = React.ComponentProps<'button'>;

const Button = ({ className, ...props }: ButtonProps) => {
  return <button className={clsx('button', className)} {...props} />;
};

export default Button;

We discovered easy methods to restrict the props {that a} part will settle for. To increase the props, we will use an intersection:

sort ButtonProps = React.ComponentProps<'button'> &  'secondary';
;

We’re now saying that Button accepts the entire props {that a} <button> component accepts plus another: variant. This prop will present up with all the opposite props we inherited from HTMLButtonElement.

Variant shows up as a prop on our Button component

We are able to add help to our Button so as to add this class as properly:

const Button = ({ variant, className, ...props }: ButtonProps) => {
  return (
    <button
      className={clsx(
        'button',
        variant === 'major' && 'button-primary',
        variant === 'secondary' && 'button-secondary',
        className,
      )}
      {...props}
    />
  );
};

We are able to now replace src/utility.tsx to make use of our new button part:

diff --git a/src/utility.tsx b/src/utility.tsx
index 978a61d..fc8a416 100644
--- a/src/utility.tsx
+++ b/src/utility.tsx
@@ -1,3 +1,4 @@
+import Button from './elements/button';
 import useCount from './use-count';

 const Counter = () => {
@@ -8,15 +9,11 @@ const Counter = () => {
       <h1>Counter</h1>
       <p className="text-7xl">{rely}</p>
       <div className="flex place-content-between w-full">
-        <button className="button" onClick={decrement}>
+        <Button onClick={decrement}>
           Decrement
-        </button>
-        <button className="button" onClick={reset}>
-          Reset
-        </button>
-        <button className="button" onClick={increment}>
-          Increment
-        </button>
+        </Button>
+        <Button onClick={reset}>Reset</Button>
+        <Button onClick={increment}>Increment</Button>
       </div>
       <div>
         <kind
@@ -32,9 +29,9 @@ const Counter = () => {
         >
           <label htmlFor="set-count">Set Depend</label>
           <enter sort="quantity" id="set-count"  />
-          <button className="button-primary" sort="submit">
+          <Button variant="major" sort="submit">
             Set
-          </button>
+          </Button>
         </kind>
       </div>
     </essential>

You’ll find the modifications above in the button branch of the GitHub repo for this tutorial.

Creating Composite Parts

One other frequent part that I usually find yourself making for myself is a part that appropriately wires up a label and enter component with the proper for and id attributes respectively. I are inclined to develop weary typing this out again and again:

<label htmlFor="set-count">Set Depend</label>
<enter sort="quantity" id="set-count"  />

With out extending the props of an HTML component, I’d find yourself slowly including props as wanted:

sort LabeledInputProps =  quantity;
  sort?: string;
  className?: string;
  onChange?: ChangeEventHandler<HTMLInputElement>;
;

As we noticed with the button, we will refactor it similarly:

sort LabeledInputProps = React.ComponentProps<'enter'> & {
  label: string;
};

Aside from label, which we’re passing to the (uhh) label that we’ll usually need grouped with our inputs, we’re manually passing props via one after the other. Will we need to add autofocus? Higher add one other prop. It will be higher to do one thing like this:

import { ComponentProps } from 'react';

sort LabeledInputProps = ComponentProps<'enter'> & {
  label: string;
};

const LabeledInput = ({ id, label, ...props }: LabeledInputProps) => {
  return (
    <>
      <label htmlFor={id}>{label}</label>
      <enter {...props} id={id} readOnly={!props.onChange} />
    </>
  );
};

export default LabeledInput;

We are able to swap in our new part in src/utility.tsx:

<LabeledInput
  id="set-count"
  label="Set Depend"
  sort="quantity"
  onChange={(e) => setValue(e.goal.valueAsNumber)}
  worth={worth}
/>

We are able to pull out the issues we have to work with after which simply move all the pieces else on via to the <enter /> part, after which simply fake for the remainder of our days that it’s a regular HTMLInputElement.

TypeScript doesn’t care, since HTMLElement is fairly versatile, because the DOM pre-dates TypeScript. It solely complains if we toss one thing utterly egregious in there.

You’ll be able to see the entire modifications above in the input branch of the GitHub repo for this tutorial.

This text is excerpted from Unleashing the Power of TypeScript, out there on Pylogix Premium and from e book retailers.