Paul
Kinchla

Type Checking HTML

Posted on | Last Updated on by Paul Kinchla Reading Time: 3 minutes
Table of Contents

I have been learning TypeScript lately and also helping build out some design system components from the ground up. It has been a process learning TypeScript while standing up a new thing but I have gained some knowledge and found some value in TypeScript. I surprised myself and here are my findings.

Note: This article is for the TypeScript skeptic who is an HTML enthusiast. This article is also for the TypeScript lover who doesn’t spend much time with HTML. These viewpoints are not mutually exclusive. This article is also for folks who write components in JSX. Let’s get into it.

Use HTML fragment anchor

When creating a new component whose main function is to return some meaningful HTML it can be difficult to determine what should be optional, required and sometimes even relevant. I find this frequently to be true when dealing with form components. This is where rest parameters are useful.

Rest parameters provide a way to pass an indefinite number of arguments to a function. Using this syntax with a component it is possible to pass any number of arbitrary properties to a component. Take this reduced example:

// component with rest props
const MyComponent = ({ message, ...rest }: MyComponentProps) => {
  return <div {...rest}>{message}</div>;
};

// invoked component with some random props
<MyComponent message="Hello World" foo={1} bar="string" yikes="true" />

// rendered HTML
<div foo="1" bar="string" yikes="true">Hello World</div>

This example, while very flexible, is also potentially dangerous in that we are not monitoring that the correct properties are being passed to the component. For those familiar with HTML, yikes is not a valid HTML attribute for a div element.

This is where TypeScript can help us see what is possible to pass to our component while checking that we don’t any pass any invalid HTML attributes. Here is the updated example with an interface.

import { HTMLAttributes } from "react";

interface MyComponentProps extends HTMLAttributes<HTMLDivElement> {
  message: string;
}

const MyComponent = ({ message, ...rest }: MyComponentProps) => {
  return <div {...rest}>{message}</div>;
};

The updated example will now ensure that the consumer of this component always passes a string for our custom message property. In addition, we are importing the HTMLAttributes interface from React and the built-in interface HTMLDivElement from TypeScript itself to allow any valid attribute to be passed to the component.

A Useful Example fragment anchor

Let’s see this in action with a TextInput component. The following is a component for a text input that has a required label property and an optional errorMessage that will surface when an error is triggered.

import { forwardRef, InputHTMLAttributes, useId } from "react";

export interface TextInputProps extends InputHTMLAttributes<HTMLInputElement> {
  label: string;
  errorMessage?: string;
}

const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
  ({ label, type = "text", errorMessage, ...rest }, ref) => {
    const errorID = `error-${useId()}`;
    const inputID = `input-${useId()}`;

    return (
      <div>
        <label htmlFor={inputID}>{label}</label>
        <input
          id={inputID}
          aria-describedby={errorMessage && errorID}
          aria-invalid={!!errorMessage}
          ref={ref}
          {...rest}
        />
        {errorMessage && <span id={errorID}>{errorMessage}</span>}
      </div>
    );
  }
);

export default TextInput;

When this component gets used we can pass any additional HTML attributes we might need depending on context. Here we have a use case for an email input where we pass in type and pattern props that get applied as HTML attributes:

<form action="/api/form-endpoint">
  // invoked TextInput for an email use case
  <TextInput 
    label="Email" 
    type="email" 
    pattern="^[A-Z0-9+_.-]+@[A-Z0-9.-]+$" 
  />
</form>;

//rendered HTML
<form action="/api/form-endpoint">
  <div>
    <label for="input-:R4mH1:">Email</label>
    <input
      type="email"
      id="input-:R4mH1:"
      aria-invalid="false"
      pattern="^[A-Z0-9+_.-]+@[A-Z0-9.-]+$"
    />
  </div>
</form

Here is what happens when we pass an invalid attribute like autoplay (only valid on audio and video elements) to our TextInput component.

Type '{ label: string; type: string; pattern: string; autoplay: true; }' is not
assignable to type 'IntrinsicAttributes & TextInputProps &
RefAttributes<HTMLInputElement>'. Property 'autoplay' does not exist on type  'IntrinsicAttributes & TextInputProps & RefAttributes<HTMLInputElement>'.
(tsserver 2322)</HTMLInputElement></HTMLInputElement>

Too Long Didn’t Read fragment anchor

With rest parameters we can support passing any HTML attribute as a prop that applies to any given HTML element, while providing useful feedback from TypeScript when invalid attributes are passed.

Back to Table of Contents