htmlreactjstypescriptcustom-data-attribute

Does React's type declarations e.g. React.HTMLAttribute<HTMLButtonElement> support the usage of custom data attributes out of the box?


I'm trying to create an object that can be spread into a React Component and this object has to include an HTML Custom Data Attribute.

import { HTMLAttributes } from 'react'

interface FooProps extends HTMLAttributes<HTMLButtonElement> {
  fooProp: string
  //'data-testid': string // do HTML Custom Attributes not come out of the box?
}

const fooObj: FooProps = {
  fooProp: 'yay',
  'data-testid': 'nay' // Object literal may only specify known properties, and ''data-testid'' does not exist in type 'Foo'.(2353)
}

const Foo = () => <button {...fooObj}></button>

In the example above, I can't seem to type my object to allow for custom data attributes without having to explicitly list them out. How do I type the object to allow for custom data attributes without explicit declarations?


Solution

  • Although custom data attributes data-* are recognized by the TypeScript compiler when using JSX syntax — e.g. this compiles without error…

    TS Playground

    import type { ReactElement } from "react";
    
    function ExampleComponent(): ReactElement {
      return (
        <button
          data-foo="bar"
          onClick={(ev) => console.log(ev.currentTarget.dataset.foo)}
        >
          Click
        </button>
      );
    }
    

    …React does not provide element attribute type aliases/interfaces which include them.

    In order to allow for custom data attributes in your own types, you can either include them explicitly at each definition…

    TS Playground

    import type { ButtonHTMLAttributes, ReactElement } from "react";
    
    type DataAttributes = Record<`data-${string}`, string>;
    
    // Explicitly intersect the indexed type:
    const buttonAttrs: ButtonHTMLAttributes<HTMLButtonElement> & DataAttributes = {
    //                                                         ^^^^^^^^^^^^^^^^
      "data-foo": "bar",
      onClick: (ev) => console.log(ev.currentTarget.dataset.foo),
    };
    
    function ExampleComponent(): ReactElement {
      return <button {...buttonAttrs}>Click</button>;
    }
    

    …or — you can approach it in a much more DRY way by using the pattern of module augmentation:

    Create a type declaration file in your project at a path that is included in your program's compilation (e.g. src/types/react_data_attributes.d.ts):

    import type {} from "react";
    
    declare module "react" {
      interface HTMLAttributes<T> {
        [name: `data-${string}`]: string;
      }
    }
    

    Ref: microsoft/TypeScript#36812 — Add import type "mod"

    Then, at each usage site, the explicit intersection will no longer be needed:

    TS Playground

    import type { ButtonHTMLAttributes, ReactElement } from "react";
    
    // Now, only the base button attributes type annotation is needed:
    const buttonAttrs: ButtonHTMLAttributes<HTMLButtonElement> = {
      "data-foo": "bar",
      onClick: (ev) => console.log(ev.currentTarget.dataset.foo),
    };
    
    function ExampleComponent(): ReactElement {
      return <button {...buttonAttrs}>Click</button>;
    }
    

    A note regarding HTMLElement subtypes:

    Each subtype (e.g. <button>, <a>, etc.) might have specialized attributes in addition what's offered in the base HTMLAttributes, so you'll need to type your element attributes accordingly for the compiler to recognize those specific attributes. Here's an example showing some of the specialized attributes for the elements above:

    TS Playground

    import type {
      AnchorHTMLAttributes,
      ButtonHTMLAttributes,
      HTMLAttributes,
    } from "react";
    
    type ButtonSpecificAttributes = Exclude<
      /* ^? type ButtonSpecificAttributes =
        | "disabled"
        | "form"
        | "formAction"
        | "formEncType"
        | "formMethod"
        | "formNoValidate"
        | "formTarget"
        | "name"
        | "type"
        | "value"
      */
      keyof ButtonHTMLAttributes<HTMLButtonElement>,
      keyof HTMLAttributes<HTMLButtonElement>
    >;
    
    type AnchorSpecificAttributes = Exclude<
      /* ^? type AnchorSpecificAttributes =
        | "download"
        | "href"
        | "hrefLang"
        | "media"
        | "ping"
        | "referrerPolicy"
        | "target"
        | "type"
      */
      keyof AnchorHTMLAttributes<HTMLAnchorElement>,
      keyof HTMLAttributes<HTMLAnchorElement>
    >;
    
    const buttonAttrs0: HTMLAttributes<HTMLButtonElement> = {
      disabled: true, /* Error
      ~~~~~~~~
      Object literal may only specify known properties, and 'disabled' does not exist in type 'HTMLAttributes<HTMLButtonElement>'.(2353) */
    };
    
    const buttonAttrs1: ButtonHTMLAttributes<HTMLButtonElement> = {
      disabled: true, // Ok
    };
    
    const anchorAttrs0: HTMLAttributes<HTMLAnchorElement> = {
      href: "https://stackoverflow.com/", /* Error
      ~~~~
      Object literal may only specify known properties, and 'href' does not exist in type 'HTMLAttributes<HTMLAnchorElement>'.(2353) */
    };
    
    const anchorAttrs1: AnchorHTMLAttributes<HTMLAnchorElement> = {
      href: "https://stackoverflow.com/", // Ok
    };