javascripttypescripttypesleafletdefinitelytyped

Typescript: Adding additional overloads to an existing abstract class


I am working on developing a set of definitions for DefinitelyTyped, for the package Leaflet.Editable. Leaflet is notable in that it uses a partially custom class implementation to allow for extending of existing types in pure JavaScript, as seen here. Simply calling import 'Leaflet.Editable' anywhere in your code will add new functionality to the existing Leaflet classes, such as the ability to enable and disable editing of certain layers.

It is not implemented in TypeScript, and as such typing is implemented in @types/leaflet, so the package I am developing (@types/leaflet-editable) has to import the leaflet namespace and extend the existing types.

I must extend the existing types rather than adding new ones because there are other libraries (such as react-leaflet) that use those types.

For example, by extending the MapOptions type (by merging the interfaces), I was able to add new properties to react-leaflet's MapComponent component, since its props extend Leaflet.MapOptions. If I were to create a new type, EditableMapOptions, it would not be extended by react-leaflet and thus I would not be able to add those properties to MapComponent.

I was able to extend several existing interfaces, and add new ones such as VertexEventHandlerFn (which represents an event callback that provides a VertexEvent).

The issue is how @types/leaflet implements the map.on('click', (event: LeafletEvent) => {}) function. Here's a snippet from the DefinitelyTyped repo:

export abstract class Evented extends Class {
    /**
     * Adds a listener function (fn) to a particular event type of the object.
     * You can optionally specify the context of the listener (object the this
     * keyword will point to). You can also pass several space-separated types
     * (e.g. 'click dblclick').
     */
    // tslint:disable:unified-signatures
    on(type: string, fn: LeafletEventHandlerFn, context?: any): this;
    on(type: 'baselayerchange' | 'overlayadd' | 'overlayremove',
        fn: LayersControlEventHandlerFn, context?: any): this;
    on(type: 'layeradd' | 'layerremove',
        fn: LayerEventHandlerFn, context?: any): this;
    ...

As you can see, it uses an abstract class that implements a generic definition for when users include multiple events separated by spaces, as well as several specific events that, when specific arguments are provided, include proper typing for their event handlers.

I wanted to add my functions to this, but I cannot find any means to add additional overloads to an existing abstract class. The thing to note here is that all these overloads of on are implemented by the same line of code in Leaflet, so the issues faced by adding new methods to an abstract class (that would later go unimplemented by the associated JavaScript) do not exist here.

I tried simply attempting to redeclare abstract class Evented to add more methods to it but TypeScript tells me it's already defined. My research only found the TypeScript documentation indicating that I should utilize mixins, which I cannot do because I need to modify the existing class, and creating a new one would not solve the problem.

Here is my current TypeScript implementation of leaflet-editable so far.


Solution

  • It might not be clear from the documentation for module augmentation, but if you want to merge into the instance type of a named class, you can do so by adding to the interface with the same name as the class. When you write class Foo {}, it treats the type named Foo as an interface, and you can merge into it. For example:

    import { Draggable, LeafletEvent } from 'leaflet';
    
    interface PeanutButterEvent extends LeafletEvent {
      chunky: boolean
    }
    
    declare module 'leaflet' {
      interface Evented {
        on(type: "peanut-butter", fn: (x: PeanutButterEvent) => void): this;
      }
    }
    
    const draggable = new Draggable(new HTMLElement()) // whatever
    draggable.on("peanut-butter", e => console.log(e.chunky ? "Chunky" : "Creamy")); // ok
    draggable.on("resize", e => console.log(e.oldSize)); // still works
    

    Here I've added another overload of on() to the interface named Evented.


    On the other hand, if you want to merge into the static type of the class, you can do so by adding to the namespace with the same name as the class. When you write class Foo {}, it treats the value named Foo as a namespace, and you can merge into it. For example:

    import { Draggable, Evented } from 'leaflet';
    
    declare module 'leaflet' {
      namespace Evented {
        export function hello(): void;
      }
    }
    Evented.hello = () => console.log("hello"); // implement if you want
    Draggable.hello(); // okay
    Draggable.mergeOptions({}); // still works
    

    Here I've added another static method named hello() to the namespace named Evented.

    Playground link to code