13

I'm using custom-elements aka web-components within Preact. The problem is that Typescript complains about elements not being defined in JSX.IntrinsicElements - in this case a check-box element:

<div className={styles.option}>
    <check-box checked={this.prefersDarkTheme} ref={this.svgOptions.darkTheme}/>
    <p>Dark theme</p>
</div>

Error message (path omitted):

ERROR in MyComponent.tsx
[tsl] ERROR in MyComponent.tsx(50,29)
      TS2339: Property 'check-box' does not exist on type 'JSX.IntrinsicElements'.

I came across the following, unfortunately not working, possible solutions:

  1. https://stackoverflow.com/a/57449556/7664765 - It's an answer not really realted to the question but it covered my problem

I've tried adding the following to my typings.d.ts file:

import * as Preact from 'preact';

declare global {
    namespace JSX {
        interface IntrinsicElements {
            'check-box': any; // The 'any' just for testing purposes
        }
    }
}

My IDE grayed out the import part and IntrinsicElements which means it's not used (?!) and it didn't worked anyway. I'm still getting the same error message.

  1. https://stackoverflow.com/a/55424778/7664765 - Also for react, I've tried to "convert" it to preact and I got the same results as for 1.

I've even found a file maintained by google in the squoosh project where they did the following to "polyfill" the support:

In the same folder as the component a missing-types.d.ts file with the following content, basically the same setup I have but with a index.ts file instead of check-bock.ts and they're using an older TS version v3.5.3:

declare namespace JSX {
  interface IntrinsicElements {
    'range-input': HTMLAttributes;
  }
}

I'm assuming their build didn't fail so how does it work and how do I properly define custom-elements to use them within preact / react components?

I'm currently using typescript@v3.8.3 and preact@10.3.4.

Matthias
  • 13,607
  • 9
  • 44
  • 60
Simon
  • 2,686
  • 2
  • 31
  • 43

4 Answers4

7

Here are the correct attributes to use, otherwise you will get an error when passing key in for example.

declare global {
  namespace JSX {
    interface IntrinsicElements {
      'xx-element1': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>; // Normal web component
      'xx-element2': React.DetailedHTMLProps<React.HTMLAttributes<HTMLInputElement>, HTMLInputElement>; // Web component extended from input
    }
  }
}
Dominic
  • 62,658
  • 20
  • 139
  • 163
6

Okay I managed to solve it using module augmentation:

declare module 'preact/src/jsx' {
    namespace JSXInternal {

        // We're extending the IntrinsicElements interface which holds a kv-list of
        // available html-tags.
        interface IntrinsicElements {
            'check-box': unknown;
        }
    }
}

Using the HTMLAttributes interface we can tell JSX which attributes are available for our custom-element:

// Your .ts file, e.g. index.ts
declare module 'preact/src/jsx' {
    namespace JSXInternal {
        import HTMLAttributes = JSXInternal.HTMLAttributes;

        interface IntrinsicElements {
            'check-box': HTMLAttributes<CheckBoxElement>;
        }
    }
}

// This interface describes our custom element, holding all its
// available attributes. This should be placed within a .d.ts file.
declare interface CheckBoxElement extends HTMLElement {
    checked: boolean;
}
Simon
  • 2,686
  • 2
  • 31
  • 43
2

With typescript 4.2.3 and preact 10.5.13, here is what works to define a custom tag name with attributes:

declare module 'preact' {
    namespace JSX {
        interface IntrinsicElements {
            'overlay-trigger': OverlayTriggerAttributes;
        }
    }
}

interface OverlayTriggerAttributes extends preact.JSX.HTMLAttributes<HTMLElement> {
    placement?: string;
}

Differences:

  • The module is 'preact' (must be quoted).
  • The namespace is JSX.
  • The IntrinsicElements value type is an interface that extends HTMLAttributes.
  • It extends HTMLAttributes via the name preact.JSX.HTMLAttributes.
  • It supplies the base element type as HTMLElement to populate the eventTarget type in props/attrs like event listeners. You could also put SVGElement if applicable.
Matthias
  • 13,607
  • 9
  • 44
  • 60
  • This works for me with TypeScript 4.3.5 and Preact 10.5.15, with the caveat that I also needed to `import 'preact'` before my declaration. – Erik Ostrom Nov 13 '21 at 00:30
1

There is a better way to do this without manually binding events.

You can use @lit-labs/react's createComponent to wrap web component to React Component.

import * as React from "react";

import { createComponent } from "@lit-labs/react";
import Slrange from "@shoelace-style/shoelace/dist/components/range/range.js";

export const Range = createComponent(React, "sl-range", Slrange,{
    change: "sl-change" // map web component event to react event
});

import { Range } from "./SlRange";

export default function App() {
  return (
    <div className="App">
      <Range max={6} min={2} step={2}></Range>
    </div>
  );
}

Edit react-web-component

Amit
  • 3,662
  • 2
  • 26
  • 34