Sunday, March 31, 2024

Extending the Properties of an HTML Ingredient in TypeScript — SitePoint

Must read


On this fast tip, excerpted from Unleashing the Energy of TypeScript, Steve exhibits you find out how to lengthen the properties of an HTML factor in TypeScript.

In a lot of the bigger functions and tasks I’ve labored on, I usually discover myself constructing a bunch of parts which can be actually supersets or abstractions on prime of the usual HTML parts. Some examples embrace customized button parts which may take a prop defining whether or not or not that button needs to be a main or secondary button, or perhaps one which signifies that it’ll invoke a harmful motion, corresponding 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 element that permits me to outline a label and an enter subject without delay. I don’t need to re-add the entire properties that an <enter /> factor takes. I would like my customized element to behave similar to an enter subject, however additionally take a string for the label and mechanically wire up the htmlFor prop on the <label /> to correspond with the id on the <enter />.

In JavaScript, I can simply use {...props} to go via any props to an underlying HTML factor. This is usually a bit trickier in TypeScript, the place I must explicitly outline what props a element will settle for. Whereas it’s good to have fine-grained management over the precise varieties that my element accepts, it may be tedious to have so as to add in kind info for each single prop manually.

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

There are a few methods that we are able to make use of:

  • For parts the place we’re creating an abstraction over only one form of factor, we are able to lengthen the properties of that factor.
  • For parts the place we need to outline totally different parts, we are able to create polymorphic parts. A polymorphic element is a element designed to render as totally different HTML parts or parts whereas sustaining the identical properties and behaviors. It permits us to specify a prop to find out its rendered factor kind. Polymorphic parts provide flexibility and reusability with out us having to reimplement the element. For a concrete instance, you possibly can take a look at Radix’s implementation of a polymorphic element.

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

Mirroring and Extending the Properties of an HTML Ingredient

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’d be capable to 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 manner an HTML button does:

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

You possibly can think about that including properties one after the other might get a bit tedious. As a substitute, we are able to inform TypeScript that we need to match the identical props that it will use for a <button> factor in React:

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

However we’ve a brand new downside. Or, relatively, we had an issue that additionally existed within the JavaScript instance and which we ignored. If somebody utilizing our new Button element 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 go up the chance to point out you find out how to use a utility kind in TypeScript to say “I need to use all of the props from an HTML button besides for one (or extra)”:

kind 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 element. If we simply needed to increase the category listing with no matter is handed in, we might try this in a number of other ways. We might simply append it to the listing:

kind 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 lessons, because it takes care of most of those sorts of issues on our behalf:

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

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

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

export default Button;

We discovered find out how to restrict the props {that a} element will settle for. To increase the props, we are able to use an intersection:

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

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

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

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

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

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 './parts/button';
 import useCount from './use-count';

 const Counter = () => {
@@ -8,15 +9,11 @@ const Counter = () => {
       <h1>Counter</h1>
       <p className="text-7xl">{depend}</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 Rely</label>
           <enter kind="quantity" id="set-count" title="set-count" />
-          <button className="button-primary" kind="submit">
+          <Button variant="main" kind="submit">
             Set
-          </button>
+          </Button>
         </kind>
       </div>
     </most important>

You could find the modifications above within the button department of the GitHub repo for this tutorial.

Creating Composite Parts

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

<label htmlFor="set-count">Set Rely</label>
<enter kind="quantity" id="set-count" title="set-count" />

With out extending the props of an HTML factor, I would find yourself slowly including props as wanted:

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

As we noticed with the button, we are able to refactor it similarly:

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

Apart 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. Can we need to add autofocus? Higher add one other prop. It could be higher to do one thing like this:

import { ComponentProps } from 'react';

kind 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 element in src/utility.tsx:

<LabeledInput
  id="set-count"
  label="Set Rely"
  kind="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 go every little thing else on via to the <enter /> element, after which simply fake for the remainder of our days that it’s a typical HTMLInputElement.

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

You possibly can see the entire modifications above within the enter department of the GitHub repo for this tutorial.

This text is excerpted from Unleashing the Energy of TypeScript, accessible on SitePoint Premium and from book retailers.





Supply hyperlink

More articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest article