Search code examples
javascripttypescriptmixins.d.ts

Using JavaScript Class Mixins with TypeScript Declaration Files


I need help using class mixins in declaration files. Specifically, when a method is defined in a mixin, typescript is not picking it up in the mixed class body:

In my case, I am applying two mixins. The first mixin - NotifyingElementMixin - provides a method called notify, and it's this method which is failing to apply to the mixed class body

notifying-element-mixin.js

export const NotifyingElementMixin = superclass =>
  class NotifyingElement extends superclass {
    /**
     * Fires a `*-changed` event.
     *
     * @param  {string}     propName Name of the property.
     * @param  {any} value  property value
     * @protected
     */
    notify(propName, value) {
      this.dispatchEvent(
        new CustomEvent(`${propName}-changed`, {
          detail: { value },
        })
      );
    }
  };
};

notifying-element-mixin.d.ts

export declare class NotifyingElement {
  public notify(propName: string, value: any): void
}

export function NotifyingElementMixin<TBase extends typeof HTMLElement>
(superclass: TBase): TBase & NotifyingElement;

The second mixin provides other properties and methods, but for the sake of this question, I've simplified the implementation

apollo-query-mixin.js

export const ApolloQueryMixin = 
  superclass => class extends superclass {
    data = null;
    is = 'Query';
  };

apollo-query-mixin.d.ts

export declare class ApolloQuery<TCacheShape, TData, TVariables, TSubscriptionData = TData> {
  data: null
  is: string
}

type Constructor<T = HTMLElement> = new (...args: any[]) => T;
export function ApolloQueryMixin<TBase extends Constructor, TCacheShape, TData, TVariables>
(superclass: TBase): ApolloQuery<TCacheShape, TData, TVariables> & TBase;

Finally, I want to export a class which applies both mixins and provides it's own methods as well. This is where I run into trouble

apollo-query.js

class ApolloQuery extends NotifyingElementMixin(ApolloQueryMixin(HTMLElement)) {
  /**
   * Latest data.
   */
  get data() {
    return this.__data;
  }

  set data(value) {
    this.__data = value;
    this.notify('data', value);
  }
  // etc
}

apollo-query.d.ts

import { ApolloQueryMixin } from "./apollo-query-mixin";
import { NotifyingElementMixin } from "./notifying-element-mixin";

export declare class ApolloQuery<TBase, TCacheShape, TData, TVariables>
extends NotifyingElementMixin(ApolloQueryMixin(HTMLElement)) {}

When I compile this, or use my IDE, I receive the error:

error TS2339: Property 'notify' does not exist on type 'ApolloQuery'.

How do I finagle typescript into picking up my inherited methods in the mixed class body?


Solution

  • Here's the mixin pattern I use, I think the key is the return constructor:

    import { LitElement, property } from "lit-element";
    
    type Constructor = new (...args: any[]) => LitElement;
    
    interface BeforeRenderMixin {
      beforeRenderComplete: Boolean;
    }
    
    type ReturnConstructor = new (...args: any[]) => LitElement & BeforeRenderMixin;
    
    export default function<B extends Constructor>(Base: B): B & ReturnConstructor {
      class Mixin extends Base implements BeforeRenderMixin {
        @property({ type: Boolean })
        public beforeRenderComplete: boolean = false;
    
        public connectedCallback() {
          super.connectedCallback();
          if (!this.beforeRenderComplete)
            this.beforeRender().then(() => (this.beforeRenderComplete = true));
        }
    
        public async beforeRender() {
          return;
        }
    
        public shouldUpdate(changedProperties: any) {
          return this.beforeRenderComplete && super.shouldUpdate(changedProperties);
        }
      }
    
      return Mixin;
    }
    

    which generates:

    import { LitElement } from "lit-element";
    declare type Constructor = new (...args: any[]) => LitElement;
    interface BeforeRenderMixin {
        beforeRenderComplete: Boolean;
    }
    declare type ReturnConstructor = new (...args: any[]) => LitElement & BeforeRenderMixin;
    export default function <B extends Constructor>(Base: B): B & ReturnConstructor;
    export {};