TypeScript-DOM-lib-generator icon indicating copy to clipboard operation
TypeScript-DOM-lib-generator copied to clipboard

CustomElementConstructor is missing observedAttributes

Open nolanlawson opened this issue 3 years ago • 2 comments

The CustomElementConstructor interface defined in lib.dom.d.ts seems to be missing the observedAttributes static property (supported in all modern browser engines).

Steps to reproduce (playground):

class MyElement extends HTMLElement {
    static observedAttributes = ['foo']
}

function define(tagName: string, ctor: CustomElementConstructor) {
    customElements.define(tagName, ctor)
    console.log(ctor.observedAttributes)
}

define('my-element', MyElement)

Expected result: no error.

Actual result: TypeScript finds an error for the line with console.log(ctor.observedAttributes):

Property 'observedAttributes' does not exist on type 'CustomElementConstructor'.(2339)

Here is a CodePen demonstrating that this does indeed work in the browser. The logged value should be ["foo"].

nolanlawson avatar May 26 '22 18:05 nolanlawson

To clarify: the observedAttributes property is optional, so my proposed change is:

 interface CustomElementConstructor {
   new (...params: any[]): HTMLElement;
+  observedAttributes?: string[];
 }

nolanlawson avatar May 26 '22 18:05 nolanlawson

It's missing all the lifecycle callbacks too. It makes it tricky to derive custom element constructors without some workarounds.

function derive(ctor: CustomElementConstructor) {
  static get observedAttributes() {
    return [...super.observedAttributes || [], "my-extra-attr"]; // error, as above
  }

  attributeChangedCallback(...params: any[]) {
    super.attributeChangedCallback?.(...params); // Property 'attributeChangedCallback' does not exist on type 'HTMLElement'.
  }
}

This isn't as straightforward to fix as adding those properties to the interface, since the constructor is defined as returning a HTMLElement instance (which is obviously incorrect). I suspect this will become more of an issue as decorators become more widely used on custom element classes.

The interface should probably look something more like this:

interface CustomElementConstructor extends HTMLElement {
  new (...params: any[]): CustomElementConstructor;
  observedAttributes?: any;
  adoptedCallback?(...params: any[]): any;
  attributeChangedCallback?(...params: any[]): any;
  connectedCallback?(...params: any[]): any;
  disconnectedCallback?(...params: any[]): any;
}

NB: I've used any types here because, technically, you can register classes with different signatures and return values as custom elements. If narrow types were used, e.g. string[] for observedAttributes, it would restrict what you could pass to customElements.define()

andyearnshaw avatar May 25 '23 19:05 andyearnshaw