'Custom props in a Button react component allowed in a specific defined interface

I have this code:

interface ICustomProps {
    'data-event-name': AllowedEvents;
    'data-event-to': string;
}

interface IButton {
    type?: ButtonTypes;
    icon?: string;
    iconPosition?: string;
    size?: ButtonSizes;
    hide?: boolean;
    onClick?(event?: React.MouseEvent<HTMLElement>): void;
    disabled?: boolean;
    text?: string;
    event?: React.HTMLProps<HTMLButtonElement> | ICustomProps;
    title?: string;
    iconSize?: IconSizes;
    iconColor?: IconColor;
    style?: React.CSSProperties;
    className?: string;
    children?: React.ReactNode;
}

My expectations were that I couldn't use any props in that Button but these declared. So, any HTMLButtonElement prop and also the defined ones, such as "title" or "icon".

But I have two problems:

  1. When I call this component I was expecting the list of allowed values for the prop "data-event-name", but the IDE didn't show them
  2. If I would write any random prop, typescript compiler should throw an error, but it doesn't. So I can call this component with random prop such as foo-prop="blah" without receiving any error. I guess is something about props with "-"

P.S.

type AllowedEvents = typeof allEvents[number];

where allEvents is an array of strings

here a reproducible case



Solution 1:[1]

I'll start with some context: here is a summary from the React docs about HTML attribute support. (Please read the docs page to view the full list of camelCased attributes):

As of React 16, any standard or custom DOM attributes are fully supported.

React has always provided a JavaScript-centric API to the DOM. Since React components often take both custom and DOM-related props, React uses the camelCase convention just like the DOM APIs:

<div tabIndex={-1} />      // Just like node.tabIndex DOM API
<div className="Button" /> // Just like node.className DOM API
<input readOnly={true} />  // Just like node.readOnly DOM API

These props work similarly to the corresponding HTML attributes, with the exception of the special cases documented above.

...

You may also use custom attributes as long as they’re fully lowercase.

With that in mind, let's get into the details of your question:


Your AllowedEvents type

Currently you have this:

TS Playground

const allEvents = ['event1', 'event2', 'event3']; // string[]
type AllowedEvents = typeof allEvents[number]; // string

allEvents is inferred as string[] by the compiler, so AllowedEvents is string. This isn't what you wanted, but you can get the result that you want by telling the compiler to use literal inference using as const, like this:

TS Playground

const allEvents = ['event1', 'event2', 'event3'] as const; // readonly ["event1", "event2", "event3"]
type AllowedEvents = typeof allEvents[number]; // "event1" | "event2" | "event3"

Now, AllowedEvents is one of those string literals!


Your IButton['event'] type

When you need to determine a type used by an import, you can use IntelliSense to discover it. In this case, you can create a bare JSX <button> and hover it to see the type of its props:

TS Playground

IntelliSense

The type that you need is: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>.

I think you want to merge this with your ICustomProps interface, but right now you're creating a union, which means x OR y:

event?: ICustomProps | React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>;

Instead, you want an intersection to create x AND y, like this:

event?: ICustomProps & React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>;

Similar to the <button> props, you can discover the inferred type for its onClick attribute using IntelliSense: it's React.MouseEventHandler<HTMLButtonElement>. If you want it to be more generic (for another type of element in your actual component), you can use React.MouseEventHandler<HTMLElement>.


Putting it all together

I renamed a few things, and removed the extra properties that should be taken care of by the HTML button attributes:

TS Playground

import {default as React} from 'react';

const allEvents = ['event1', 'event2', 'event3'] as const;
type AllowedEvents = typeof allEvents[number];

interface CustomAttributes {
  'data-event-name': AllowedEvents;
  'data-event-to': string;
}

interface ButtonProps {
  attributes?: CustomAttributes & React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>;
  icon?: string;
  iconPosition?: string;
  hide?: boolean;
  text?: string;
  children?: React.ReactNode;
}

const Button = (props: ButtonProps) => {
  return (<button {...props.attributes}>{props.text}</button>);
};

const TestComponent = () => {
  return (
    <Button
      attributes={{
        'data-event-name': 'not-existing-event', // typed custom attribute, incorrect value type
        'not-existing-prop-with-dash': true, // unknown custom attribute, React is ok with this
        notExistingProp: 1, // unknown custom attribute, React is ok with this
        style={padding: '0.5rem'}, // React.CSSProperties | undefined
      }}
      hide={false}
      text="My button"
    />
  );
};

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 jsejcksn