Typescript Extension Methods demo
Good news! Typescript/Javascript already has extension methods. And they work well with latest version of TS and your editor. Take a look at ./src/index.ts on how to do.
NOTE THOUGH: It's probably wise to include all extensions upfront, rather than through dynamic import
s, at least for interface extensions, since otherwise you'd be modifying already used prototypes, thereby likely invalidating optimizations done by JIT compilers for Javascript (TurboFan, IonMonkey).
There's been a lot of talk about Typescript Extension methods in many places. Primarily at https://github.com/microsoft/TypeScript/issues/9, but lots of other places too.
The TL;DR of that thread is something like (my summary):
myFoo.myExtension()
Foo.prototype.myExtension
) or some kind of callsite rewriting (like myExtension(myFoo)
)? (https://github.com/microsoft/TypeScript/issues/9#issuecomment-52329918)Now, personally I think the most important thing extension methods bring is discovery. I can type a .
after a variable and see a list of suggestions (auto-complete). And, that list can be expanded with new methods. This is powerful. Of course we can already write functions that accomplish essentially the same by simply passing free functions and pass arguments to them, but the discovery is harder. And the code is less beautiful. So I created this suggestion https://github.com/microsoft/TypeScript/issues/35280, but Ryan Cavanaugh thought the perf budget for such a dev support would be too high. So I set up to create a plugin myself. Only to realize that...
Remember bullet 3. above. We just ditched prototype extension, the natural Javascript way of extending objects. With the risk of missing some arguments, I think the cons amount to:
But, symbol
s are meant to deal with problem 1. So to see if that worked, I created this repo just as a proof of concept. Extensions to classes extend their respective prototypes, but not with string
s, but symbol
s. Thus there can be no collision. There is actually also an even simpler solution, where you can index these prototypes directly with the function, but that's not compatible with current Typescript, which requires indexed properties to be of number
/string
/symbol
. If that allowed function too it would work fine. So something like
// myExtension.ts
export function myExtension(this: Foo) { ... }
Foo.prototype[myExtension] = myExtension;
// foo.ts
import { myExtension } from './myExtension'
...
foo[myExtension]();
Extensions to interfaces extend Object
(yes Object
, read on...).
Extending Object
may sound weird. We could end up with 1000 methods or more on Object.prototype
. I created a performance test ./src/perftest.js, to examine the performance difference between a prototype with 1000 methods (indexed by symbol
) vs just one. If we allow for warmup there is no statistical difference. neither in V8 (Safari/Chrome/Node) nor SpiderMonkey (Firefox). Focusing on V8, that applies to TurboFan (the optimized compiler), but not to Ignition (the bytecode interpreter) where the big prototype is 8-13% slower on my machine. I argue this is negligable still, since any hot (reasonably coded code path) would run in TurboFan, and extension method access for interfaces is very likely not a significant part of a typicaly CPU consumption.
Furthermore, extensions allow for working with just interfaces and plain objects (from JSON deserialization), without converting to custom model classes. This skips an extra conversion step, likely gaining performance.
.
style auto-complete via Typescript's Language Service. But, they currently require typescript@next (probably until 3.9) - they work partially well with prior versions.Object
like ext
. This could return a Proxy
that could scan all the symbols available through the prototype chain and provide non-symbol style accessors for everything that doesn't collide. Thus you could type something like Date.ext.isLeapYear()
, rather than Date[isLeapYear]()
in the REPL. Exercise left to the reader.