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

1

1

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?

Gracie answered 22/12, 2023 at 3:12 Comment(4)
Can you edit the question to include a minimal reproducible example? I can't reproduce: tsplay.dev/WJBx5WGrainfield
Appreciate it man, I just updated my question with a repro exampleGracie
^ @Gracie Please add the minimal reproducible example to the content of the question — links (e.g. to the TS playground) are potentially very helpful supportive material, but aren't substitutes for question content.Grainfield
Re: the playground link you posted: You've created a type and shown a data structure which can't be assigned to that type… but what's the issue? It seems like you might be making some assumptions about the types exported by @types/react, but the actual problem that you're trying to solve by creating a custom type is still not clear. Please demonstrate the problem with a minimal reproducible example that clarifies your expectations and what about them is not being met.Grainfield
G
2

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
};
Grainfield answered 4/1 at 0:56 Comment(7)
Is there a TypeScript issue to upstream interface HTMLAttributes<T> { [name: `data-${string}`]: string; }?Snobbish
^ @Snobbish "a TypeScript issue to upstream…": I'm not sure that I understand — these types are from the React types package. Here's a link to the relevant line in the latest commit.Grainfield
A right, DefinitelyTyped/react not TypeScript nor React. I mean whether anyone has considered to simply add this index property to the original HTMLAttributes definition, instead of everyone having to do module augmentation? This seems to be a more common issue than you'd think, see the linked questions I've closed as dupes earlier. I think it might be worthwhile creating an issue or even a PR, unless this has already been done but declined for some reason.Snobbish
^ @Snobbish Ah ok — I see that activity. Re: issue/PR: Not that I can tell — here's a link to a view of results in that repo for issues and PRs matching the query react data attributes.Grainfield
You fancy creating one?Snobbish
^ @Snobbish I'm willing, but procedure in that repo is quite a bit different from others — I'll take a look and report back if I engage.Grainfield
^ @Snobbish I've started a discussion in the repo.Grainfield

© 2022 - 2024 — McMap. All rights reserved.