Stencil version:
@stencil/[email protected]
I'm submitting a:
[x] bug report
[ ] feature request
[ ] support request => Please do not submit support requests here, use one of these channels: https://stencil-worldwide.herokuapp.com/ or https://forum.ionicframework.com/
Current behavior:
When using a Stencil component library from a React + TS application, we receive type errors. Specifically:
TypeScript error in /home/sam/my-marketplace/src/App.tsx(27,9):
Property 'manifold-marketplace' does not exist on type 'JSX.IntrinsicElements'. TS2339
25 | </header>
26 | <article>
> 27 | <manifold-marketplace />
| ^
28 | </article>
29 | </div>
30 | );
Expected behavior:
I should be able to use my web components from a React + TypeScript application without JSX.IntrinsicElements type errors.
Steps to reproduce:
npm i && npm startRelated code:
import React from "react";
import "@manifoldco/ui/dist/manifold/manifold.css";
import { defineCustomElements } from "@manifoldco/ui/dist/loader";
import logo from "./logo.svg";
import "./App.css";
defineCustomElements(window);
const App: React.FC = () => {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
<article>
<manifold-marketplace />
</article>
</div>
);
};
export default App;
Other information:
The React project used to demonstrate the issue is using an alpha release of our component library. You can find that alpha release in this git tag if you wish to download the source code.
The alpha release uses @stencil/[email protected], but we have had this problem with previous versions as well. Before Stencil 1 was released, we had a workaround for the issue, described here under the section labeled "TYPESCRIPT + JSX SETUP". This workaround no longer works.
Similar to your previous workaround, I have something set up for stencil 1+
https://github.com/ionic-team/stencil/issues/1090#issuecomment-501124883
for convenience I will post again here.
for people struggling with this, I've gotten things working this way:
Edit: Updated thanks to @sslotsky for figuring out the last bits for this solution.
// register-web-components.ts
/* eslint-disable */
import { defineCustomElements, Components, JSX as LocalJSX } from 'stencil-library/dist/loader';
import { DetailedHTMLProps, HTMLAttributes } from 'react';
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>
type StencilProps<T> = { [P in keyof T]: Omit<T[P], "ref"> };
type ReactProps<T> = {
[P in keyof T]: DetailedHTMLProps<HTMLAttributes<T[P]>, T[P]>
};
type StencilToReact<
T = LocalJSX.IntrinsicElements,
U = HTMLElementTagNameMap
> = StencilProps<T> & ReactProps<U>;
declare global {
export namespace JSX {
interface IntrinsicElements extends StencilToReact {}
}
}
defineCustomElements(window);
// index.ts
// ...
import './register-web-components';
// ...
eslint will shout about using declare global but this is keeping me going for now. gets auto complete working
@Nibblesh thanks so much for this! Extremely helpful. There is one detail giving me trouble with this solution, though: we are using hooks, and when I use a ref on my web component, it's expecting a callback function instead of a RefObject. Any ideas there?
@sslotsky could you put an example of what you would like to work? I don't fully understand the scenario without an example
@Nibblesh sure :smile:
const link = React.useRef<HTMLManifoldButtonLinkElement>(null);
if (link.current) {
link.current.addEventListener(MANIFOLD_BUTTON_LINK_CLICK, someUrl);
}
...
// somewhere inside render
<manifold-button-link ref={link} href={href} preserveEvent>
Go to all products
</manifold-button-link>
FWIW, I do have something working, but I can't figure out a way to loop through the keys.
type ToReact<T> = DetailedHTMLProps<HTMLAttributes<T>, T>;
declare global {
export namespace JSX {
interface IntrinsicElements {
'manifold-button-link': Components.ManifoldButtonLink &
ToReact<HTMLManifoldButtonLinkElement>;
}
}
}
By doing it this way you are using just the interface defined in Components and extending it with all the React specific stuff. But from what I can tell, there is no mapping from component tag name to the Components namespace, so I need to do this manually for every tag name that I use.
Maybe this might be usable for you
// register-web-components.ts
import { defineCustomElements, JSX as LocalJSX } from 'stencil-library/dist/loader';
import { HTMLAttributes, DetailedHTMLProps } from 'react';
import { Omit } from 'types';
type StencilToReact<T> = {
[P in keyof T]?: Omit<DetailedHTMLProps<HTMLAttributes<T[P]>, T[P]>, 'className'> & {
class?: string;
} & T[P];
} ;
declare global {
export namespace JSX {
interface IntrinsicElements extends StencilToReact<LocalJSX.IntrinsicElements> {
}
}
}
defineCustomElements(window)
Not quite, see error below :smile:
I think the HTMLManifoldButtonLinkElement bit is important. In the error below we see that we're using ManifoldbuttonLink instead.
src/components/Error/index.tsx:34:10 - error TS2322: Type '{ children: string; ref: RefObject<HTMLManifoldButtonLinkElement>; href: string; preserveEvent: true; }' is not assignable to type 'Pick<DetailedHTMLProps<HTMLAttributes<ManifoldButtonLink>, ManifoldButtonLink>, "hidden" | "dir" | "slot" | "style" | "title" | "color" | "ref" | "key" | "defaultChecked" | ... 244 more ... | "onTransitionEndCapture"> & { ...; } & ManifoldButtonLink'.
Type '{ children: string; ref: RefObject<HTMLManifoldButtonLinkElement>; href: string; preserveEvent: true; }' is not assignable to type 'Pick<DetailedHTMLProps<HTMLAttributes<ManifoldButtonLink>, ManifoldButtonLink>, "hidden" | "dir" | "slot" | "style" | "title" | "color" | "ref" | "key" | "defaultChecked" | ... 244 more ... | "onTransitionEndCapture">'.
Types of property 'ref' are incompatible.
Type 'RefObject<HTMLManifoldButtonLinkElement>' is not assignable to type 'string | ((instance: ManifoldButtonLink | null) => void) | RefObject<ManifoldButtonLink> | null | undefined'.
Type 'RefObject<HTMLManifoldButtonLinkElement>' is not assignable to type 'RefObject<ManifoldButtonLink>'.
Type 'HTMLManifoldButtonLinkElement' is not assignable to type 'ManifoldButtonLink'.
Types of property 'style' are incompatible.
Type 'CSSStyleDeclaration' is not assignable to type '{ [key: string]: string | undefined; }'.
Index signature is missing in type 'CSSStyleDeclaration'.
34 <manifold-button-link ref={link} href={href} preserveEvent>
I won't know until you try it but maybe this
// register-web-components.ts
/* eslint-disable */
import { defineCustomElements, JSX as LocalJSX } from 'stencil-library/dist/loader';
import { HTMLAttributes, DetailedHTMLProps } from 'react';
import { Merge, Omit } from 'ts-essentials';
type StencilToReactElements<T = LocalJSX.IntrinsicElements> = {
[P in keyof T]?: T[P] & Omit<DetailedHTMLProps<HTMLAttributes<T[P]>, T[P]>, 'className'> & {
class?: string;
};
} ;
type StencilToReactRef<T = HTMLElementTagNameMap> = {
[P in keyof T]: {
ref?: DetailedHTMLProps<HTMLAttributes<T[P]>, T[P]>['ref']
}
};
type StencilToReact<T = LocalJSX.IntrinsicElements, U = HTMLElementTagNameMap>= StencilToReactElements<T> & StencilToReactRef<U>;
declare global {
export namespace JSX {
interface IntrinsicElements extends StencilToReact {
}
}
}
defineCustomElements(window)
This doesn't work either :disappointed: TypeScript still thinks that ref should be the callback type defined by Stencil:
((elm?: HTMLManifoldButtonLinkElement | undefined) => void)
If it helps @Nibblesh, the component library in question is public, and the version I'm testing against for this issue is available as an alpha release. You could try rendering one of the elements and attaching a ref to it with the useRef hook. If you have time to give that a shot, it would be _super_ helpful! :pray: I think it would save time in the long run compared to how we're currently approaching it. I really appreciate how much time you've already dedicated to this!
I gave it a crack this morning without the best luck. Spent ages on this and looking through the type defs as although your issues don't affect me yet I am sure it will be an issue for me at some point.
That being said It might be possible to get this to work using a HoC.
Supply the rest of the props to the web component as per normal, then wrap it in a HoC which clones the child and converts the react ref object into a compatible callback method and return the cloned element.
I don't have the time to give this a try ATM but I'll come back to it when I have a chance.
@Nibblesh I think I got it, modifying your most recent example a bit!
import { Components, JSX as LocalJSX } from '@manifoldco/ui';
import { DetailedHTMLProps, HTMLAttributes } from 'react';
type StencilProps<T> = {
[P in keyof T]?: Omit<T[P], 'ref'>;
};
type ReactProps<T> = {
[P in keyof T]?: DetailedHTMLProps<HTMLAttributes<T[P]>, T[P]>;
};
type StencilToReact<T = LocalJSX.IntrinsicElements, U = HTMLElementTagNameMap> = StencilProps<T> &
ReactProps<U>;
declare global {
export namespace JSX {
interface IntrinsicElements extends StencilToReact {}
}
}
@sslotsky awesome!! I felt like I was close but I'm glad to hear you figured it out!
I am going to update my original answer with this =)
edit: just to call this one out, it works with typescript 3.5+ but didn't work on the version I had on my project at the time which was 3.3.4. I might have been making it unnecessarily hard for myself to try and figure this out by not upgrading earlier
Nice @Nibblesh! We were using 3.5.2, I suppose every one of these discussions should start with environment info :sweat_smile:
Now, do you happen to know if there's a way for TypeScript to know about the HTML attributes that a Stencil component accepts? It seems like TypeScript allows me to add absolutely any-dasherized-attribute to any custom element. Looking through my Stencil project it seems like those types aren't even listed anywhere. Only their camelCased corollaries are. Am I missing something?
I think that seems like it should be happening during the compilation step (might be worth writing up a feature request), wherever they're writing out the definitions for the camel case stuff, they could run a regex to convert camel to dash and register the same value there.
Have you tried using Ionics way of doing this?
https://github.com/ionic-team/ionic/blob/master/packages/react/src/components/createComponent.tsx
We're also using Stencil components in a React/Typescript library. I copied an older version of the createComponent.tsx to our codebase some time ago, so I'm not sure the current one is working - but as the Ionic components seem to be working, I suppose their implementation should be fine :)
Thanks @sslotsky and @Nibblesh! In my case this is what worked:
import { applyPolyfills, defineCustomElements, JSX as LocalJSX } from '@wizeline/trip-planner-web-components/dist/loader'
import { DetailedHTMLProps, HTMLAttributes } from 'react';
type StencilProps<T> = {
[P in keyof T]?: Omit<T[P], 'ref'> | HTMLAttributes<T>;
};
type ReactProps<T> = {
[P in keyof T]?: DetailedHTMLProps<HTMLAttributes<T[P]>, T[P]>;
};
type StencilToReact<T = LocalJSX.IntrinsicElements, U = HTMLElementTagNameMap> = StencilProps<T> &
ReactProps<U>;
declare global {
export namespace JSX {
interface IntrinsicElements extends StencilToReact { }
}
}
I needed to add | HTMLAttributes<T> to StencilProps; if not I got issues if I wanted to assign an id or other attributes.
If anyone has a problem with other unsupported HTML attribute you can also assign AllHTMLAttributes instead (that will allow other attributes like name or src; not sure if useful though)
I set up a monorepo to experiment Stencil and React integration using the info in this thread. See https://github.com/jagreehal/react-stencil-dx. When I get time this week I going to follow up on the suggestion by @schadenn
@sslotsky another happy dev :-) thanks for the tip!
Thx @sslotsky and @adrian-marcelo-gallardo, same to me in an Ionic React app!
import { applyPolyfills, defineCustomElements, JSX as LocalJSX } from '@deckdeckgo/lazy-img/dist/loader'
import { DetailedHTMLProps, HTMLAttributes } from 'react';
type StencilProps<T> = {
[P in keyof T]?: Omit<T[P], 'ref'> | HTMLAttributes<T>;
};
type ReactProps<T> = {
[P in keyof T]?: DetailedHTMLProps<HTMLAttributes<T[P]>, T[P]>;
};
type StencilToReact<T = LocalJSX.IntrinsicElements, U = HTMLElementTagNameMap> = StencilProps<T> &
ReactProps<U>;
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace JSX {
interface IntrinsicElements extends StencilToReact { }
}
}
applyPolyfills().then(() => {
defineCustomElements(window);
});
Maybe there is something in the bigger picture that I don't get. But why cannot generated types simply have an export of interface IntrinsicElements. The types are generated correctly and ready for use but simply not exported. Why?
Example. If components.d.ts only could have an export in front of interface IntrinsicElements:
//components.d.ts
`declare namespace LocalJSX {
interface MyComponent {
'first'?: string;
'last'?: string;
'middle'?: string;
}
export interface IntrinsicElements {
'my-component': MyComponent;
}
}`
// dev.d.ts in my react project
`import { JSX as LocalJSX } from '@mypackage/webcomp/types/components';
declare global {
namespace JSX {
interface IntrinsicElements extends LocalJSX.IntrinsicElements {}
}
}`
Thanks for the issue! This issue is being closed due to inactivity. If this is still an issue with the latest version of Stencil, please create a new issue and ensure the template is fully filled out.
Thank you for using Stencil!
Reopening since this is still an issue as mentioned in #2729.
Most helpful comment
@Nibblesh I think I got it, modifying your most recent example a bit!