How do I add Typescript definitions for Leaflet plugins

后端 未结 3 1757
Happy的楠姐
Happy的楠姐 2021-02-06 15:41

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

相关标签:
3条回答
  • 2021-02-06 16:06

    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.

    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:

    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!

    0 讨论(0)
  • 2021-02-06 16:12

    It actually does now. You should just

    import * as L from 'leaflet';
    import 'leaflet-easybutton/src/easy-button';
    
    
    0 讨论(0)
  • 2021-02-06 16:18

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

    Your new L.EasyButton takes a 'EasyButtonOptions' object for the constructor. This does not match the 'L.easyButton' examples

    A L.easyButton has the following options: function easyButton(icon: string, onClick: (btn: any, map: any)=>void, title?: string, id?: string) : Control.EasyButton;

    An EasyButtonOptions doesn't have a 'icon', 'onClick', 'title' or 'id' instead it takes:

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

    So therefore, what is the equivalent of new L.EasyButton() that matches L.easyButton('fa fa-icon', () => '..', 'title) ?

    0 讨论(0)
提交回复
热议问题