Skip to content

Commit 614d305

Browse files
fix: resolve TypeScript interface conflicts between component methods and HTMLElement (#6282)
* fix: resolve TypeScript interface conflicts between component methods and HTMLElement Fixes #4467 When Stencil components have @method() decorators on methods with names that match HTMLElement methods (like 'focus', 'blur', 'click'), TypeScript fails to compile due to incompatible method signatures between the component interface and HTMLStencilElement. This commit implements conflict detection and resolution by: - Adding comprehensive HTML_ELEMENT_METHODS constant covering HTMLElement, Element, Node, and EventTarget method names - Detecting method name conflicts during type generation - Using TypeScript's Omit utility type to exclude conflicting methods from component interface and re-declare them with correct signatures - Preserving JSDoc documentation for conflicting methods - Maintaining backward compatibility for components without conflicts The solution generates interfaces like: `interface HTMLMyButtonElement extends Omit<Components.MyButton, "focus">, HTMLStencilElement` with explicit method overrides when conflicts are detected. Includes comprehensive tests covering single conflicts, multiple conflicts, mixed scenarios, and JSDoc preservation. * prettier
1 parent e253ceb commit 614d305

3 files changed

Lines changed: 414 additions & 7 deletions

File tree

src/compiler/types/constants.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// HTML Element method names that might conflict with component methods
2+
// Based on MDN documentation for HTMLElement, Element, and Node interfaces
3+
export const HTML_ELEMENT_METHODS = new Set([
4+
// HTMLElement methods
5+
'attachInternals',
6+
'blur',
7+
'click',
8+
'focus',
9+
'hidePopover',
10+
'showPopover',
11+
'togglePopover',
12+
13+
// Element methods
14+
'after',
15+
'animate',
16+
'append',
17+
'attachShadow',
18+
'before',
19+
'checkVisibility',
20+
'closest',
21+
'computedStyleMap',
22+
'getAnimations',
23+
'getAttribute',
24+
'getAttributeNames',
25+
'getAttributeNode',
26+
'getAttributeNodeNS',
27+
'getAttributeNS',
28+
'getBoundingClientRect',
29+
'getClientRects',
30+
'getElementsByClassName',
31+
'getElementsByTagName',
32+
'getElementsByTagNameNS',
33+
'getHTML',
34+
'hasAttribute',
35+
'hasAttributeNS',
36+
'hasAttributes',
37+
'hasPointerCapture',
38+
'insertAdjacentElement',
39+
'insertAdjacentHTML',
40+
'insertAdjacentText',
41+
'matches',
42+
'moveBefore',
43+
'prepend',
44+
'querySelector',
45+
'querySelectorAll',
46+
'releasePointerCapture',
47+
'remove',
48+
'removeAttribute',
49+
'removeAttributeNode',
50+
'removeAttributeNS',
51+
'replaceChildren',
52+
'replaceWith',
53+
'requestFullscreen',
54+
'requestPointerLock',
55+
'scroll',
56+
'scrollBy',
57+
'scrollIntoView',
58+
'scrollIntoViewIfNeeded',
59+
'scrollTo',
60+
'setAttribute',
61+
'setAttributeNode',
62+
'setAttributeNodeNS',
63+
'setAttributeNS',
64+
'setCapture',
65+
'setHTML',
66+
'setHTMLUnsafe',
67+
'setPointerCapture',
68+
'toggleAttribute',
69+
70+
// Node methods
71+
'appendChild',
72+
'cloneNode',
73+
'compareDocumentPosition',
74+
'contains',
75+
'getRootNode',
76+
'hasChildNodes',
77+
'insertBefore',
78+
'isDefaultNamespace',
79+
'isEqualNode',
80+
'isSameNode',
81+
'lookupNamespaceURI',
82+
'lookupPrefix',
83+
'normalize',
84+
'removeChild',
85+
'replaceChild',
86+
87+
// EventTarget methods
88+
'addEventListener',
89+
'dispatchEvent',
90+
'removeEventListener',
91+
]);

src/compiler/types/generate-component-types.ts

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { addDocBlock, dashToPascalCase, sortBy } from '@utils';
22

33
import type * as d from '../../declarations';
4+
import { HTML_ELEMENT_METHODS } from './constants';
45
import { generateEventListenerTypes } from './generate-event-listener-types';
56
import { generateEventTypes } from './generate-event-types';
67
import { generateMethodTypes } from './generate-method-types';
@@ -27,6 +28,10 @@ export const generateComponentTypes = (
2728
const eventAttributes = generateEventTypes(cmp, typeImportData, tagNameAsPascal);
2829
const { htmlElementEventMap, htmlElementEventListenerProperties } = generateEventListenerTypes(cmp, typeImportData);
2930

31+
// Check for method conflicts with HTMLElement
32+
const conflictingMethods = methodAttributes.filter((method) => HTML_ELEMENT_METHODS.has(method.name));
33+
const hasMethodConflicts = conflictingMethods.length > 0;
34+
3035
const componentAttributes = attributesToMultiLineString(
3136
[...propAttributes, ...methodAttributes],
3237
false,
@@ -35,15 +40,20 @@ export const generateComponentTypes = (
3540
const isDep = cmp.isCollectionDependency;
3641
const jsxAttributes = attributesToMultiLineString([...propAttributes, ...eventAttributes], true, areTypesInternal);
3742

43+
// Generate the element interface with method conflict resolution
44+
const elementInterface = hasMethodConflicts
45+
? generateElementInterfaceWithConflictResolution(
46+
htmlElementName,
47+
tagNameAsPascal,
48+
conflictingMethods,
49+
htmlElementEventListenerProperties,
50+
cmp.docs,
51+
)
52+
: generateStandardElementInterface(htmlElementName, tagNameAsPascal, htmlElementEventListenerProperties, cmp.docs);
53+
3854
const element = [
3955
...htmlElementEventMap,
40-
addDocBlock(
41-
` interface ${htmlElementName} extends Components.${tagNameAsPascal}, HTMLStencilElement {`,
42-
cmp.docs,
43-
4,
44-
),
45-
...htmlElementEventListenerProperties,
46-
` }`,
56+
...elementInterface,
4757
` var ${htmlElementName}: {`,
4858
` prototype: ${htmlElementName};`,
4959
` new (): ${htmlElementName};`,
@@ -60,6 +70,72 @@ export const generateComponentTypes = (
6070
};
6171
};
6272

73+
/**
74+
* Generate element interface when there are no method conflicts
75+
* @param htmlElementName the name of the HTML element interface
76+
* @param tagNameAsPascal the component tag name in PascalCase
77+
* @param htmlElementEventListenerProperties event listener properties for the element
78+
* @param docs JSDoc documentation for the component
79+
* @returns array of interface definition lines
80+
*/
81+
function generateStandardElementInterface(
82+
htmlElementName: string,
83+
tagNameAsPascal: string,
84+
htmlElementEventListenerProperties: string[],
85+
docs: d.CompilerJsDoc | undefined,
86+
): string[] {
87+
return [
88+
addDocBlock(
89+
` interface ${htmlElementName} extends Components.${tagNameAsPascal}, HTMLStencilElement {`,
90+
docs,
91+
4,
92+
),
93+
...htmlElementEventListenerProperties,
94+
` }`,
95+
];
96+
}
97+
98+
/**
99+
* Generate element interface with method conflict resolution using intersection types
100+
* @param htmlElementName the name of the HTML element interface
101+
* @param tagNameAsPascal the component tag name in PascalCase
102+
* @param conflictingMethods array of method metadata that conflicts with HTMLElement methods
103+
* @param htmlElementEventListenerProperties event listener properties for the element
104+
* @param docs JSDoc documentation for the component
105+
* @returns array of interface definition lines
106+
*/
107+
function generateElementInterfaceWithConflictResolution(
108+
htmlElementName: string,
109+
tagNameAsPascal: string,
110+
conflictingMethods: d.TypeInfo,
111+
htmlElementEventListenerProperties: string[],
112+
docs: d.CompilerJsDoc | undefined,
113+
): string[] {
114+
const methodOverrides = conflictingMethods
115+
.map((method) => {
116+
const optional = method.optional ? '?' : '';
117+
let docBlock = '';
118+
if (method.jsdoc) {
119+
docBlock =
120+
[` /**`, ...method.jsdoc.split('\n').map((line) => ' * ' + line), ` */`].join('\n') +
121+
'\n';
122+
}
123+
return `${docBlock} "${method.name}"${optional}: ${method.type};`;
124+
})
125+
.join('\n');
126+
127+
return [
128+
addDocBlock(
129+
` interface ${htmlElementName} extends Omit<Components.${tagNameAsPascal}, ${conflictingMethods.map((m) => `"${m.name}"`).join(' | ')}>, HTMLStencilElement {`,
130+
docs,
131+
4,
132+
),
133+
methodOverrides,
134+
...htmlElementEventListenerProperties,
135+
` }`,
136+
];
137+
}
138+
63139
const attributesToMultiLineString = (attributes: d.TypeInfo, jsxAttributes: boolean, internal: boolean) => {
64140
const attributesStr = sortBy(attributes, (a) => a.name)
65141
.filter((type) => {

0 commit comments

Comments
 (0)