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
While intelisense work as expected, if you hover over
d
and look at the type, it is considerably uglier than a hand crafted version.I defined
methods
separately and not inline onObject.assign
because the compiler gets confused if I do.If you don't want to use
this
inside the methods, you don't need to typethis
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 wrongthis
.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 referenceChart
inside a function defined in the object assigned tomethods
0 comments:
Post a Comment