Jonathan
Jonathan

Reputation: 32868

How can I add a custom attribute to React's HTMLAttributes from a library?

Let's say I'm building a re-usable library for use in a React application. This library looks for certain DOM elements with certain attributes and values.

For example, it looks for DIV with an attribute of 'foo' that can equal 'bar' or 'baz'.

Both the library and its consumers are written in Typescript.

Now when I try to instantiate a DIV element in my consumer code:

// consumer/Consumer.tsx

import * as React from 'react';
import 'library';

export const Consumer = () =>
  <div foo="bar">Hello</div>;

I get a type error

Type '{ children: string; type: string; onClick: () => void; foo: string; }' is not assignable to type 'DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>'. Property 'foo' does not exist on type 'DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>'.ts(2322) (property) JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>

I can fix this by adding merged a module declaration and interface declaration to my consumer's code:

// consumer/Consumer.tsx

import * as React from 'react';
import 'library';

declare module 'react' {
  interface HTMLAttributes<T> {
    readonly foo?: 'bar' | 'baz';
  }
}

export const Consumer = () =>
  <div foo="bar">Hello</div>;

However, I think this should really be done in my library's code, rather than the consumer's code. After all, it's the library's job to define its contracts, not its consumer's job. The consumer should be able to import whichever interfaces the library defines from the library.

So I tried moving the declaration to my library code. I wasn't sure where to put it, so I decided to just append it to the index.ts file, which exports all the library's components:

// library/src/index.ts

export * from './LibraryComponent1.tsx';
export * from './LibraryComponent2.tsx';

// added declaration here:
declare module 'react' {
  interface HTMLAttributes<T> {
    readonly foo?: 'bar' | 'baz';
  }
}

Unfortunately this doesn't work. When I recompile the library and re-link it to my consumer, I get the same type errors as above.

I've tried:

What am I missing here? Any thoughts?

Upvotes: 1

Views: 3037

Answers (2)

mayid
mayid

Reputation: 1775

Say you want to accept extra attributes in your custom component, and use the spread operator (like ...rest) for the preexisting attributes. Here´s how you do it:

interface Props{
  icon?: string; 
}

type Button = Props & React.HTMLProps<HTMLButtonElement> & React.HTMLAttributes<HTMLButtonElement>;

function Button({ 
  icon,
  ...rest
}: Button) {
  return (
    <button 
      {...rest}
    >
     {icon && <span>{icon}</span>}
     {children}       
    </button>
}

Upvotes: -1

Ryan Cogswell
Ryan Cogswell

Reputation: 80976

First of all, I am not a TypeScript user myself (though I've dealt with it a little bit in working with third-party packages), so I apologize if this answer betrays some of my ignorance in this area.

It seems that you are trying to put an inappropriate responsibility on your library. It makes sense to me that your library should be able to say that foo is a valid property for a div within one of the library's components, but you seem to be suggesting that the library should be able to have a side-effect of changing the validation of elements that are NOT produced by components of the library.

To make sure the distinction is clear. It seems natural for me for the library to have something like (assuming that the appropriate TypeScript would be applied to make this valid):

// library code
export const LibComponent1 = (props) => {
   return <div foo="bar">{props.children}</div>
}

and then allow a consumer to use LibComponent1 without issue. But it seems like bad library behavior to allow changing the TypeScript validation for all divs in components defined outside of the library to allow a property of "foo". If multiple libraries did things like this, they could easily conflict with one another or hide issues in other code.

Upvotes: 2

Related Questions