34

Anyone know how to properly add/extend all native HTML element attributes with custom ones?

With the TypeScript documentation for merging interfaces, I thought that I could just do this:

interface HTMLElement {
    block?: BEM.Block;
    element?: BEM.Element;
    modifiers?: BEM.Modifiers;
}

<div block="foo" />; // error

But I get the following Intellisense error in vscode 1.6.1 (latest):

[ts] Property 'block' does not exist on type 'HTMLProps'.

The HTMLProps to which they are referring is React.HTMLProps<T> and the div element is declared to use it like so:

namespace JSX {
    interface IntrinsicElements {
        div: React.HTMLProps<HTMLDivElement>
    }
}

I tried redeclaring the div, but to no avail.

Related: https://github.com/Microsoft/TypeScript/issues/11684

Edit: Here's what ended up working for me:

declare module 'react' {
    interface HTMLAttributes<T> extends DOMAttributes<T> {
        block?: string
        element?: string
        modifiers?: Modifiers // <-- custom interface
    }
}
jedmao
  • 10,224
  • 11
  • 59
  • 65
  • @MadaraUchiha What about `extend`ing `React.HTMLProps`? Or even merging declarations with `React.HTMLProps`? – Zev Spitz Feb 03 '17 at 09:16
  • @ZevSpitz I've tried both and they didn't work well. Extending doesn't help, because I can't force it to use my interface, it'll just use `React.HTMLProps`, and merging declarations simply didn't work, it completely ignored them. If you can make a case where it does work, consider posting it as an answer. – Madara's Ghost Feb 05 '17 at 14:18
  • @MadaraUchiha _merging the declarations simply didn't work_ I presume you mean `namespace React { interface HTMLProps { /*custom elements here*/ } }`? I don't know what the `HTMLProps` declaration looks like, it may be necessary to match it. – Zev Spitz Feb 05 '17 at 15:12
  • I have a question: why is this needed? Is there a problem with using `data-*` properties? – Andrew Li Feb 05 '17 at 21:10
  • can you create playground, codepen or jsfiddle ? – Aqdas Feb 06 '17 at 14:01

7 Answers7

15

Looks like in older versions of type definition files (v0.14) the interfaces were simply declared under a global React namespace, so previously you could use the standard merging syntax.

declare namespace React {

    interface HTMLProps<T> extends HTMLAttributes, ClassAttributes<T> {
    }
}

However the new version of d.ts file (v15.0) have declared everything inside a module. Since modules do not support merging, to the best of my knowledge the only option right now seems to be module augmentation: https://www.typescriptlang.org/docs/handbook/declaration-merging.html

I did the following experiment and it worked for me:

import * as React from 'react';

declare module 'react' {
     interface HTMLProps<T> {
        block?:string;
        element?:string;
        modifiers?:string;
    }

}

export const Foo = () => {

    return (
        <div block="123" element="456">
        </div>
    )
};

Obviously this is quite tedious, you could put the augmentation code in another file as shown in the example from the typescript handbook, and import it:

import * as React from 'react';
import './react_augmented';

But it's still quite dirty. So maybe it's best to address the issue with the contributors of the type definition file.

Edwin
  • 1,184
  • 9
  • 9
8

I wanted to use glamor's createElement replacement which adds a css prop to every element.

To add to the accepted answer, module augmentation seems to do the trick but HTMLProps only worked for non-input elements. The correct interfaces to extend seems to be HTMLAttributes and SVGAttributes.

declare module 'react' {
  interface HTMLAttributes<T> {
    css?: any
  }

  interface SVGAttributes<T> {
    css?: any
  }
}

To avoid importing the module augmentation in every component, re-export createElement:

// createElement.ts
import { createElement } from 'glamor/react'

declare module 'react' {
  interface HTMLAttributes<T> {
    css?: any
  }

  interface SVGAttributes<T> {
    css?: any
  }
}

export default createElement

Then tell TS to use our createElement for JSX with this tsconfig:

{
  "compilerOptions": {
    "jsx": "react",
    "jsxFactory": "createElement"
  }
}

Usage:

// MyComponent.tsx
import createElement from './createElement'

export default function MyComponent() {
  return <div css={{ color: 'red' }} />
}
jschr
  • 1,866
  • 2
  • 16
  • 21
2

An up-to-date example (May 2019)

React type definition file (by default - index.d.ts when staring with create-react-app) contain list of all the standard HTML elements, as well as known attributes.

In order to allow custom HTML attributes, you need to define it's typing. Do that by expanding HTMLAttributes interface:

declare module 'react' {
  interface HTMLAttributes<T> extends AriaAttributes, DOMAttributes<T> {
    // extends React's HTMLAttributes
    custom?: string;
  }
}
yuval.bl
  • 4,890
  • 3
  • 17
  • 31
2

Vue 3 with TSX

// src/index.d.ts

import * as vue from 'vue';
declare module 'vue' {
  interface HTMLAttributes {
    className?: string;
    vHtml?: string;
    frameBorder?: string;
  }
}

Ace
  • 335
  • 3
  • 7
0

For vue, the following works:

declare module 'vue-tsx-support/types/dom' {
    interface InputHTMLAttributes  {
        autocorrect: string;
        autocapitalize
    }
}
Poul K. Sørensen
  • 16,950
  • 21
  • 126
  • 283
0

Adding non-standard attributes from React TypeScript Cheatsheet

// react-unstable-attributes.d.ts
import "react";

declare module "react" {
  interface ImgHTMLAttributes<T> extends HTMLAttributes<T> {
    loading?: "auto" | "eager" | "lazy";
  }
}
0

I needed to add module global for Vue 3 *.vue files to work and explicitly add react types in package.json devDependencies as such "@types/react": "^18.0.14".

// src/html.d.ts

declare global {
  declare module '@types/react' {
    interface HTMLAttributes<T> extends AriaAttributes, DOMAttributes<T> {
      dataCy?: string;
      class?: string | string[] | Record<string, boolean> | Record<string, boolean>[];
    }
  }
}