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
};
@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