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
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)
// 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);
}
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 { }
}
}
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
}
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;
(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'
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!
It actually does now. You should just
import * as L from 'leaflet';
import 'leaflet-easybutton/src/easy-button';
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) ?