简体   繁体   中英

Implementing the D3 “reusable chart” pattern in TypeScript

The code in section 2 below (working example here ) is based on the code in section 1 but changed to use arrow functions, and it is based on Mike Bostock's pattern in Toward Resusable Charts , namely returning a function that has other functions on it.

If I try to run either the code in section 1 or 2 in typescript (demo here ) it says the methods addToChart and stop do not exist on type (selection: any) => () => void .

How can I get typescript to recognize the functions properties ( addToChart and stop in this case) added to the returned function?

section 1

const mychart = function (){
  let stop = false;
  const chart = function(selection){
    function tick(){
      console.log("tick");
    }
    return tick;
  };

  // Adding a function to the returned 
  // function as in Bostock's reusable chart pattern
  chart.addToChart = function(value){ 
    console.log("addToChart");
    return chart;
  };

  chart.stop = function(){
    return stop = true;
  }

  return chart;
}

const a = mychart();
const tick = a();
tick(); //logs tick
a.addToChart(); //logs "addToChart"

section 2

const mychart = () => {
  let stop = false;

  const chart = (selection) => {
    function tick(){
      console.log("tick");
    }
    return tick;
  };

  chart.addToChart = (value) => {
    console.log("addToChart");
    return chart;
  };

  chart.stop = () => {
    return stop = true;
  }

  return chart;
} 

const a = mychart();
const tick = a();
tick(); //logs tick
a.addToChart(); //logs "addToChart"

You can define a hybrid type , ie an interface describing both the function's signature as well as its properties. Given your code it could be something like this:

interface IChart {
    (selection: any): any;
    // Use overloading for D3 getter/setter pattern
    addToChart(): string;               // Getter
    addToChart(value: string): IChart;  // Setter
}

Since you should avoid any like the plague this might need some further refinement, but it should be enough to get you started. Furthermore, to allow for a D3-ish getter/setter pattern you can overload the addToChart function in the interface declaration.

Integrating this interface as a type in your reusable code pattern now becomes pretty straightforward:

const mychart = (): IChart => {

  // Private value exposed via closure
  let value: string|undefined;

  const chart = <IChart>((selection) => {
    // Private logic
  });

  // Public interface
  // Implementing a  D3-style getter/setter.
  chart.addToChart = function(val?: string): any {
    return arguments.length ? (value = val, chart) : value;
  };

  return chart;
} 

const chart = mychart();

console.log(chart.addToChart())   // --> undefined       
chart.addToChart("Add");          // Sets private value to "Add".
console.log(chart.addToChart())   // --> "Add"       

Have a look at the executable playground demo .

I was wondering if you could use interface / class :

interface IChart {
    constructor: Function;
    addToChart?: (number) => Chart;
    stop: () => boolean;
}

class Chart implements IChart {

    private _stop = false;
    constructor( selection ) {
        // content of tick funciton here
    }

    public addToChart = function (n: number) {
        return this;
    }
    public stop = function () {
        return this._stop = true;
    }

}

let mychart = function () {
    let stop = false;
    let chartNew: Chart = new Chart(1);
    return chartNew;
}; 

You can use Object.assign to create a hybrid type (a function that has extra properties), without having to define a dedicated interface. You can define the functions inside the original separately, so you can have multiple signatures for each function, and you can even type the this parameter if you want to access the object through this instead of chart

let mychart = function () {
    let isStopped = false;
    let value = "";


    type Chart = typeof chart;
    // Complex method with multiple signatures
    function addToChart(): string 
    function addToChart(newValue: string): Chart
    function addToChart(newValue?: string): string | Chart {
        if(newValue != undefined){
            value = newValue;
            chart.stop()
            return chart;
        }else{
            return value;
        }
    }
    // We can specify the type for this if we want to use this
    function stop(this: Chart) {
        isStopped = true;
        return this; // instead of chart, either is usable
    }
    var methods = {
        addToChart,
        stop,

        // inline function, we can return chart, but if we reference the Chart type explicitly the compiler explodes 
        stop2() {
            isStopped = true;
            return chart;
        }
    };
    let chart = Object.assign(function (selection) {
        function tick() {

        }
        return tick;
    }, methods);
    return chart;
}; 
let d = mychart();

d("");
d.addToChart("").addToChart();
d.addToChart();
d.stop();
d.stop().addToChart("").stop2().stop()

Notes

  1. While intelisense work as expected, if you hover over d and look at the type, it is considerably uglier than a hand crafted version.

  2. I defined methods separately and not inline on Object.assign because the compiler gets confused if I do.

  3. If you don't want to use this inside the methods, you don't need to type this explicitly. I showed how to use it, just for the sake of completeness, using chart may be easier and it ensures that we don't have to deal with somebody passing in the wrong this .

  4. While the example above works, there are certain cases in which the compiler gives up on inference and will type the return of mychart as any. One such case is when we reference Chart inside a function defined in the object assigned to methods

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM