Oh TypeScript!!!
I’ve written before about the weirdness of TypeScript and instance functions of classes.
To summarise. Let’s say we’re using map
on an array of strings, and we therefore need to give it a function (item: string) => Something
– and maybe we have a SomethingFactory
which can use its internal state to make our things from the item:
export default class SomethingFactory { private recipes: Map<string, Recipe>; ... public createSomething(recipeName: string): Something { // use the recipes to create a Something } }
So far, this seems reasonable. The abstract example is easier than the details of a real-world example. However, let’s imagine that we’re using this to load records from a web service, or produce objects from a template or whatever.
The calling code for this might look like this:
<
>const somethingFactory: SomethingFactory = …; // type not needed here, but included for clarity const list: string[] = [ ‘first’, ‘second’, ‘third’ ]; const converted: Something[] = list.map((item) => somethingFactory.createSomething(item));
The above works. However, with my use function references, rather than write functions to call functions hat on, it also looks inefficient. Can’t we instead use:
// as createSomething IS a function, can't we just reference it? const converted: Something[] = list.map(somethingFactory.createSomething);
No.
For reasons that I don’t entirely get, the function as declared this way doesn’t directly imply the this
that you need for it to refer to the internal state of the class. This is essentially a static reference to an instance function.
Boy that’s annoying.
But then I realised something accidentally while creating some helper code recently. Let’s look at what happens if we treat the function as a field of the object, rather than a method on it. This shouldn’t make sense.
public get createSomething(): (recipeName: string) => Something { return (recipeName: string) => { // use the recipes to create a Something }; }
Looks harder doesn’t it?
It made sense when I wrote it in my particular object, which was a test helper that was trying to provide a function to simulate some other part of the system. I needed to get a function, and this getter made sense. Let’s see how we can use it.
Can I call it?
// this does a GET and then an invocation on the // returned function somethingFactory.createSomething('foo');
Oh. That looks… kinda normal…
Can I use it with map?
const items = list.map(somethingFactory.createSomething);
That works too… this is because the this
is essentially being baked into the ad-hoc function object that’s returned when we invoke the getter using the syntactic sugar of somethingFactory.<getter>
.
Something somewhere is clunky, but if you follow this pattern, you end up with an object that you can use the way your instincts might like to use it. That said, I wonder whether the runtime efficiency of this second model is worse than the first. We probably shouldn’t worry about it.
In the class-native form, I suspect the transpiler/runtime is inserting a this
binding into the invocation behind the scenes. In the second form, we’ve got a factory method creating an ad-hoc function, we’ve ALSO got a this binding when we use the getter… it may well be less efficient.
However, if runtime performance is an issue, the optimisations would probably not land on one of these.
Will I be Doing This in Future?
No. It’s too weird and it lends itself to surprise when two forms of the function operate differently depending on whether someone’s done the trick.
Published on Java Code Geeks with permission by Ashley Frieze, partner at our JCG program. See the original article here: Oh TypeScript!!! Opinions expressed by Java Code Geeks contributors are their own. |
this book is very good!