Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I add Typescript definitions for Leaflet plugins

I would like to use easy-buttons plugin with Typescript https://github.com/CliffCloud/Leaflet.EasyButton/blob/master/src/easy-button.js and but it doesn't come with Typescript annotations.

like image 458
Chui Tey Avatar asked Jun 10 '17 11:06

Chui Tey


2 Answers

Step 1 - light up the errors

The first step is to use the sample code as-is without Typescript annotations, and the errors will start lighting up in VS Code.

// sample.ts
L.easyBar([
  L.easyButton('fa-file', function(btn, map){ }),
  L.easyButton('fa-save', function(btn, map){ }),
  L.easyButton('fa-edit', function(btn, map){ }),
  L.easyButton('fa-dot-circle-o', function(btn, map){ })
]).addTo(map);

To which we create a file called 'easy-button.d.ts' and refer to it in our Typescript file.

// sample.ts
import "./easy-button"
L.easyBar([
  L.easyButton('fa-file', function(btn, map){ }),
  L.easyButton('fa-save', function(btn, map){ }),
  L.easyButton('fa-edit', function(btn, map){ }),
  L.easyButton('fa-dot-circle-o', function(btn, map){ })
]).addTo(map);

And there's nothing in easy-button.d.ts

// easy-button.d.ts
// empty for now

The error says

error TS2339: Property 'easyBar' does not exist on type 'typeof L'.
error TS2339: Property 'easyButton' does not exist on type 'typeof L'.

Which is fair enough because we haven't defined these yet.

If you refer to definition of easyBar and easyButton here and here, you will find there's a bit of magic occuring in the original Javascript declarations. It appears that these two functions don't take any arguments, but in reality they do.

L.easyButton = function(/* args will pass automatically */){
  var args = Array.prototype.concat.apply([L.Control.EasyButton],arguments);
  return new (Function.prototype.bind.apply(L.Control.EasyButton, args));
};

This function is going to call new on the L.Control.EasyButton class. The parameters are somewhat cryptic but you can infer them from this line that gives:

initialize: function(icon, onClick, title, id)

Step 2 - add the typings

// easy-button.d.ts
declare namespace L {
    function easyBar();
    function easyButton();
}

and now we are a bit closer:

error TS2346: Supplied parameters do not match any signature of call target

and that's quite obvious because we supplied 2 parameters 'fa-edit' and a callback to easyButton but we didn't declare any in our arguments. Our second attempt now looks like this:

// easy-button.d.ts
declare namespace L {
    function easyBar(buttons: any[]);
    function easyButton(icon: string, onClick: (btn: any, map: any)=>void);
}

and now all the Typescript warnings have gone away. But there's more that can be done. For one, easyButton actually takes 4 arguments. That's easy to fix - observe how optional arguments have a ? suffix:

// easy-button.d.ts
declare namespace L {
    function easyBar(buttons: any[]);
    function easyButton(icon: string, onClick: (btn: any, map: any)=>void, title?: string, id?: string);
}

Step 3 - provide return values

The easyButton method actually returns an L.Control.EasyButton instance. Currently, the Typescript definition implies the easyButton returns type any. We don't want that! Typescript is helpful only when we provide typings.

declare namespace L {
    function easyBar(buttons: Control.EasyButton[]): Control.EasyBar;
    function easyButton(icon: string, onClick: (btn: any, map: any)=>void, title?: string, id?: string) : Control.EasyButton;

    namespace Control {
        class EasyButton { };
        class EasyBar { };
    }
}

Typescript starts providing useful warnings again:

error TS2339: Property 'addTo' does not exist on type 'EasyBar'.

This is because EasyBar subclasses L.Control we need to bring that definition into our definition file.

declare namespace L {
    function easyBar(buttons: Control.EasyButton[]): Control.EasyBar;
    function easyButton(icon: string, onClick: (btn: any, map: any)=>void, title?: string, id?: string) : Control.EasyButton;

    namespace Control {
        class EasyButton extends L.Control { }
        class EasyBar extends L.Control { }
    }
}

Step 4 - provide constructor arguments to EasyButton and EasyBar

If you try to instantiate a new EasyButton, code completion suggests that you should pass in a L.ControlOptions object to configure this. Actually we need to define our own options.Code completion

declare namespace L {
    function easyBar(buttons: Control.EasyButton[], options?: EasyBarOptions): Control.EasyBar;
    function easyButton(icon: string, onClick: (btn: any, map: any)=>void, title?: string, id?: string) : Control.EasyButton;

    interface EasyBarOptions {
        position?: ControlPosition
        id?: string
        leafletClasses?: boolean
    }

    interface EasyButtonOptions {
        position?: ControlPosition
        id?: string
        type?: 'replace'|'animate'
        states?: any
        leafletClasses?: boolean
        tagName?: string
    }

    namespace Control {
        class EasyButton extends L.Control {
            constructor(options?: EasyButtonOptions)
        }
        class EasyBar extends L.Control {
            constructor(options?: EasyBarOptions)
        }
    }
}

Things look better now on code completion: new EasyButton code completion

However, I cheated on the states option. I declared that as any. In actuality it ought to be

    interface EasyButtonOptions {
        position?: ControlPosition
        id?: string
        type?: 'replace'|'animate'
        states?: EasyButtonState[]
        leafletClasses?: boolean
        tagName?: string
    }

    interface EasyButtonState {
        stateName: string
        onClick: () => void
        title: string
        icon: string
    }

Step 5 - add jsdoc hints

Typescript will provide helpful comments to users of this plugin. Here's an example of how we might provide documentation for easyButton

   /**
     * Creates a easyButton
     * @param icon e.g. fa-globe
     * @param onClick the button click handler
     * @param label on the button
     * @param an id to tag the button with
     * @example
     * var helloPopup = L.popup().setContent('Hello World!');
     *
     * L.easyButton('fa-globe', function(btn, map){
     *      helloPopup.setLatLng(map.getCenter()).openOn(map);
     *  }).addTo( YOUR_LEAFLET_MAP );
     */
    function easyButton(
        icon: string,
        onClick: (btn: Control.EasyButton, map: L.Map) => void,
        title?: string,
        id?: string): Control.EasyButton;

Step 6 Make modifications to augment leaflet types definitions

(Starting with Leaflet 1.2.0) Remove the namespace declaration:

declare namespace L {

and replace it with module augmentation:

import * as L from 'leaflet'
declare module 'leaflet' {

your test code should now read like this:

import * as L from 'leaflet'
import 'easy-button'

Step 7 integrate into the original project sources

Open up node_modules\leaflet-easybutton\package.json and add the following line below the style entry:

  "main": "src/easy-button.js",
  "style": "src/easy-button.css",
  "typings": "src/easy-button.d.ts",

Move our easy-button.d.ts into node_modules/leaflet-easybutton/src, and test that everything still works.

Then submit a pull request so that every one can benefit from the work!

like image 104
Chui Tey Avatar answered Nov 11 '22 00:11

Chui Tey


It actually does now. You should just

import * as L from 'leaflet';
import 'leaflet-easybutton/src/easy-button';

like image 24
Vincent GODIN Avatar answered Nov 11 '22 01:11

Vincent GODIN