Thursday, March 29, 2018

Implementing the D3 “reusable chart” pattern in TypeScript

Leave a Comment

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" 

3 Answers

Answers 1

You can define a hybrid type, i.e. 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.

Answers 2

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; };  

Answers 3

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

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment