Skip to content

CustomElementConstructor is missing observedAttributes #1338

Open

Description

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"].

Activity

  1. nolanlawson commented on May 26, 2022

    @nolanlawson
    Author

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

     interface CustomElementConstructor {
       new (...params: any[]): HTMLElement;
    +  observedAttributes?: string[];
     }
  2. andyearnshaw commented on May 25, 2023

    @andyearnshaw

    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()

  3. Ankith-B commented on Oct 11, 2026

    @Ankith-B

    I'd like to fix this. I've prepared a change that adds

    observedAttributes?: readonly string[];
    

    to CustomElementConstructor. I used readonly string[] rather than string[] so existing classes that use as const tuples or getters returning readonly arrays keep type-checking. I tested the example from this issue, as const, getters, classes without the property, and the derive-a-subclass pattern from Andy Earnshaw (@andyearnshaw)'s comment, and npm test passes.

    On Andy Earnshaw (@andyearnshaw)'s note about narrow types: this does mean a class whose observedAttributes isn't a string list (e.g. a Set, which the spec technically allows) would now get a type error. If you'd rather avoid that, observedAttributes?: any would match his suggestion instead.

    One implementation question: CustomElementConstructor is a callback function in the IDL, and emitCallBackFunction only emits signatures, so the simplest change adds the property line to its existing overrideSignatures in overridingTypes.jsonc. If you'd prefer, I can instead teach the emitter to support properties on callback functions. Which would you like?

    The lifecycle callbacks raised in the thread change the constructor's return type, so I'd keep those out of this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions