Chinwobble
Chinwobble

Reputation: 652

typescript - representing module pattern in types

I have the following code that I am trying to make strongly typed so that it is easier to maintain.

However for the menu variable I get the following error:

[ts]
Type '(x: number, y: number) => void' is not assignable to type 'ContextMenu'.
  Property 'items' is missing in type '(x: number, y: number) => void'.



import * as d3 from "d3";
import "d3-selection-multi";

interface ContextMenu {
    (x: number, y: number) : void;
    items(items?: string[]): string[] | this;
    remove(): void;
}

export function contextMenu(): ContextMenu {
    var height,
        width, 
        margin = 0.1, // fraction of width
        items = [], 
        rescale: boolean = false, 
        style = {
            'rect': {
                'mouseout': {
                    "fill": 'rgb(244,244,244)', 
                    "stroke": 'white', 
                    "strokeWidth": '1px'
                }, 
                'mouseover': {
                    "fill": 'rgb(200,200,200)'
                }
            }, 
            'text': {
                'fill': 'steelblue', 
                'font-size': '13'
            }
        }; 

    var menu: ContextMenu = function (x:number, y:number) {
        menu.remove();
        scaleItems();

        // Draw the menu
        d3.selectAll('svg.chart')
            .append('g').attr('class', 'context-menu')
            .selectAll('tmp')
            .data(items).enter()
            .append('g').attr('class', 'menu-entry')
            .style('cursor', 'pointer')
            .on('mouseover', function() {
                d3.select(this).select('rect').styles((<any>style).rect.mouseover) })
            .on('mouseout', function() {
                d3.select(this).select('rect').styles((<any>style).rect.mouseout) });

        d3.selectAll('.menu-entry')
            .append('rect')
            .attr('x', x)
            .attr('y', (d, i) => y + (i * height))
            .attr('width', width)
            .attr('height', height)
            .styles((<any>style).rect.mouseout);

        d3.selectAll('.menu-entry')
            .append('text')
            .text((d: string) => d)
            .attr('x', x)
            .attr('y', (d, i) => y + (i * height))
            .attr('dy', height - margin / 2)
            .attr('dx', margin)
            .styles((<any>style).text);

        // Other interactions
        d3.select('body')
            .on('click', function() {
                d3.select('.context-menu').remove();
            });
    }

    menu.remove = function() {
        d3.selectAll(".context-menu").remove();
    };

    menu.items = function(_?) {
        return (!arguments.length) 
        ? items 
        :(items = _, rescale = true, menu);
    }

    // Automatically set width, height, and margin;
    function scaleItems() {
        if (!rescale) {
            return;
        }
        d3.selectAll('svg').selectAll('tmp')
            .data(items).enter()
            .append('text')
            .text(d => d)
            .styles(<any>style.text)
            .attr('x', -1000)
            .attr('y', -1000)
            .attr('class', 'tmp');

        var z = d3.selectAll('.tmp')
            .nodes()
            .map((x:any) => x.getBBox());

        width = d3.max(z.map(x => x.width));
        margin = margin * width;
        width =  width + 2 * margin;
        height = d3.max(z.map(x => x.height + margin / 2 ));

        // cleanup
        d3.selectAll('.tmp').remove();
        rescale = false;
    }
    return menu;
}

Howe can I make the code compile but keep the same code style idiomatic D3?

Upvotes: 0

Views: 118

Answers (1)

Herrington Darkholme
Herrington Darkholme

Reputation: 6315

Sadly, there is no idiomatic way to extend function in your case. The only fallback is to cast menu function to any.

var menu: ContextMenu = function (x:number, y:number) {
    // ....
} as any

TypeScript has another functionality calls "namespace merging" to accommodate extending function literal.

function menu () {}
namespace menu {
  export function remove() {}
}
menu.remove() // compiles

However, namespace can only appears on top level of a module or nested in another namespace. You cannot declare it in a function closure. So in this case you have to fallback to any, any way.

Upvotes: 1

Related Questions