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?
Although custom data attributes data-*
are recognized by the TypeScript compiler when using JSX syntax — e.g. this compiles without error…
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…
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;
}
}
Then, at each usage site, the explicit intersection will no longer be needed:
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:
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
};