Closed RyanCavanaugh closed 8 years ago
@alonbardavid see https://github.com/Microsoft/TypeScript/issues/9#issuecomment-74302592 for why call site rewriting really, truly, really just does not work.
@RyanCavanaugh To take your example code from https://github.com/Microsoft/TypeScript/issues/9#issuecomment-74302592 I don't entirely see why some amount of call site rewriting can't work at all. For instance could we do something like:
// Setup
class Rectangle {
constructor(public width: number, public height: number) {}
}
// Do not be distracted by exact syntax here
extension function getArea(this s: Rectangle ) {
return s.width * s.height;
}
// end setup
// Challenge course begins
interface HasArea {
getArea(): number;
}
function fn1(x: HasArea) {
console.log(x.getArea());
}
fn1(new Rectangle(10, 10));
var a: any = Math.random() > 0.5 ? new Rectangle(3, 3) : { getArea: function() { return 3; } };
console.log(a.getArea());
var s = new Rectangle(5, 5);
var f = s.getArea.bind(s);
console.log(f);
class Square {
constructor(public size: number) { }
getArea() { return this.size * this.size; }
}
var box: Square|Rectangle = /* ... anything ...*/
console.log(box.getArea());
box.getArea.call(box);
var boxes: HasArea[] = [new Square(5), new Rectangle(10, 10)];
boxes.forEach(b => {
console.log(b.getArea());
});
First gets compiled down to typescript like:
// Setup
class Rectangle {
constructor(public width: number, public height: number) {}
}
class __ExtendRectangleBacking
{
constructor (public Instance: Rectangle)
{
this.getArea = __ExtendRectangleBacking.prototype.getArea.bind (this);
}
getArea ()
{
debugger;
if (this.Instance["getArea"] !== undefined)
{
return (this.Instance["getArea"].apply (this.Instance, Array.prototype.slice.call (arguments)));
}
var s = this.Instance;
return (s.width * s.height);
}
}
var __ExtendRectangle = function (Target: Rectangle)
{
return (new __ExtendRectangleBacking (Target));
};
// end setup
// Challenge course begins
interface HasArea {
getArea(): number;
}
function fn1(x: HasArea)
{
console.log(x.getArea());
}
fn1(__ExtendRectangle (new Rectangle(10, 10)));
var a: any = Math.random() > 0.5 ? __ExtendRectangle (new Rectangle(3, 3)) : { getArea: function() { return 3; } };
console.log(a.getArea());
var s = new Rectangle(5, 5);
var f = __ExtendRectangle (s).getArea.bind(s);
console.log(f);
class Square {
constructor(public size: number) { }
getArea() { return this.size * this.size; }
}
// Worst case:
var box: Square | Rectangle = new Square (137);
console.log(__ExtendRectangle (<Rectangle>/*Probably*/box).getArea());
__ExtendRectangle (<Rectangle>/*Probably*/box).getArea.call(box);
var boxes: (HasArea | Rectangle) [] = [new Square(5), new Rectangle(10, 10)];
boxes.forEach(b => {
console.log(__ExtendRectangle (<Rectangle>/*Probably*/b).getArea());
});
And then emited as javascript like:
// Setup
var Rectangle = (function () {
function Rectangle(width, height) {
this.width = width;
this.height = height;
}
return Rectangle;
}());
var __ExtendRectangleBacking = (function () {
function __ExtendRectangleBacking(Instance) {
this.Instance = Instance;
this.getArea = __ExtendRectangleBacking.prototype.getArea.bind(this);
}
__ExtendRectangleBacking.prototype.getArea = function () {
debugger;
if (this.Instance["getArea"] !== undefined) {
return (this.Instance["getArea"].apply(this.Instance, Array.prototype.slice.call(arguments)));
}
var s = this.Instance;
return (s.width * s.height);
};
return __ExtendRectangleBacking;
}());
var __ExtendRectangle = function (Target) {
return (new __ExtendRectangleBacking(Target));
};
function fn1(x) {
console.log(x.getArea());
}
fn1(__ExtendRectangle(new Rectangle(10, 10)));
var a = Math.random() > 0.5 ? __ExtendRectangle(new Rectangle(3, 3)) : { getArea: function () { return 3; } };
console.log(a.getArea());
var s = new Rectangle(5, 5);
var f = __ExtendRectangle(s).getArea.bind(s);
console.log(f);
var Square = (function () {
function Square(size) {
this.size = size;
}
Square.prototype.getArea = function () { return this.size * this.size; };
return Square;
}());
// Worst case:
var box = new Square(137);
console.log(__ExtendRectangle(box).getArea());
__ExtendRectangle(box).getArea.call(box);
var boxes = [new Square(5), new Rectangle(10, 10)];
boxes.forEach(function (b) {
console.log(__ExtendRectangle(b).getArea());
});
If the extension method returns s than accessing width gets converted to:
box.SomeMethodThatReturnsS ().width
Gets converted to:
__ExtendRectangle(box).SomeMethodThatReturnsS ().Instance.width
Any thoughts?
You've "cheated" here
var a: any = Math.random() > 0.5 ? new Rectangle(3, 3) : { getArea: function() { return 3; } };
console.log(a.getArea());
The declared type of a
is any
. There's no justification to wrap the call to new Rectangle
in a __ExtendRectangle
call here but not wrap the same call on the line var s = new Rectangle(5, 5);
. They're indistinguishable from the compiler's perspective.
The GC implications of this are terrifying - you're causing something like 5 extra allocations per function call even in places where 0 should be needed.
Okay. On point number one or "cheating" just adjust it so that items only get wrapped when the extension method gets called. I am guessing something like this:
// Setup
class Rectangle {
constructor(public width: number, public height: number) {}
}
class __ExtendRectangleBacking
{
constructor (public Instance: Rectangle)
{
this.getArea = __ExtendRectangleBacking.prototype.getArea.bind (this);
}
getArea ()
{
if (this.Instance["getArea"] !== undefined)
{
return (this.Instance["getArea"].apply (this.Instance, Array.prototype.slice.call (arguments)));
}
var s = this.Instance;
return (s.width * s.height);
}
}
// Challenge course begins
interface HasArea {
getArea(): number;
}
function fn1(x: HasArea)
{
console.log(x.getArea());
}
fn1(new Rectangle(10, 10));
/* Okay. I admit I don't see how to get around this one: */
var a: any = Math.random() > 0.5 ? new Rectangle(3, 3) : { getArea: function() { return 3; } };
console.log(a.getArea());
var s = new Rectangle(5, 5);
var f = __ExtendRectangle (s).getArea.bind(s);
console.log(f);
class Square {
constructor(public size: number) { }
getArea() { return this.size * this.size; }
}
// Worst case:
var box: Square | Rectangle = new Square (137);
console.log(__ExtendRectangle (<Rectangle>/*Probably*/box).getArea());
__ExtendRectangle (<Rectangle>/*Probably*/box).getArea.call(box);
var boxes: (HasArea | Rectangle) [] = [new Square(5), new Rectangle(10, 10)];
boxes.forEach(b => {
console.log(__ExtendRectangle (<Rectangle>/*Probably*/b).getArea());
});
Thanks for pointing that out. Ahh well. I was just hoping that extension methods might be possible in TypeScript and If possible suggest a way to do it.
I think for those that really want extension methods it could be done as an add-in or a preprocessor
@SamuelEnglard
Do any such things already exist?
@KingDavid12 in theory #6508 could eventually allows this. In the short term the TS language service could be used to process the syntax tree and do the very transformation suggested above in a step before the compiler runs. If such a system currently exists I don't know, but I vaguely remember people talking about such things in other issues.
I mean, if it could be accomplished correctly through a syntactic transform, we'd do it. The problem is that it can't.
Based on this article, [(http://perfectionkills.com/extending-native-builtins/)] extension methods/(properties?) should be syntactic sugar for a "static" object, with the same semantics as C#. This ensures that extensions follow the SOLID rules and don't allow existing objects to be modified
static class StringExtensions {
public static isUpper(this value: string): boolean {
... logic;
}
}
var myString: string = "Hello World";
var result: boolean = myString.isUpper();
var StringExtensions = {
isUpper: function(value) {
...logic;
}
}
var myString = "Hello World";
var result = StringExtensions.isUpper(myString);
@series0ne your comment suggests you haven't read the thread. This has been brought up and explained why it doesn't work several times
If you want to see the closest real thing in TS/JS of extensions, look at RxJS.
They combine extending with granular importing of functions but it's obviously very controlled and wouldn't work automatically for say ... an interface
.
So, I'm honestly really surprised that this has been talked to death and nothing has been worked out yet. I'm also surprised this was labeled 'out of scope'. That is very short-sighted, especially since extensions belong in TypeScript more than they belong in C# for all the same reasons that TypeScript decided to use a structural type-system in the first place.
This is gonna be a long one, and I apologize for that, but I think it has to be long to set up the case properly. I can see a lot of confusion as to the "what" and "why" for extensions.
If I'm going to make a case for extensions, I need to make sure everyone knows what problem they are trying to solve. Yep, they solve a problem that exists in TypeScript today; they're not just a "nice thing to have". They facilitate a kind of programming that can be extremely powerful and useful. I think the best way to do this is to teach everyone the problem that structural typing solved in TypeScript's youth and how that leads to extensions.
When the powers that be started to work on TypeScript, they almost immediately ran into a problem: JavaScript is very promiscuous with the structure of its objects. In particular, two objects in JavaScript can have entirely different prototypes, but still have the same properties and much of the same behavior. If these were to bounce their way into TypeScript code through the same entry-point, it would need the equipment to deal with them.
Let's take a look at a very simple kind of structure, the 2D vector: { x: number, y: number }
Just how many ways can you think of obtaining such a structure in TypeScript? Here are few!
// Importing one from a third-party library.
import VectorPro from 'lib/vector-pro';
// Making your own vector class.
class MyVector {
constructor(public x: number, public y: number) { }
/* ... other stuff ... */
}
// Coming in from your library's public API.
export function gimmieVectorsNomNom(v: { x: number, y: number }) { /* do noms */ }
// A raw JavaScript object.
let v1 = { x: 0, y: 0 };
Now, imagine writing a library where all those types can find their way into your code. TypeScript solved this problem by allowing you to wrangle them all into an interface
based on their common structure.
interface VectorLike {
x: number;
y: number;
}
But you have a new problem. While you can handle the structure of a vector, you can't easily attach behavior to it. But you are a clever object-oriented programmer. Wrappers are a perfect solution!
class VectorWrapper<T extends VectorLike> implements VectorLike {
get x(): number { return this.source.x; }
set x(v: number) { this.source.x = v; }
get y(): number { return this.source.y; }
set y(v: number) { this.source.y = v; }
constructor(public source: T) {}
length(): number { /* length stuff */ }
add(other: { x: number, y: number }): this { /* add stuff, return this */ }
scale(scalar: number): this { /* scaling stuff, return this */ }
}
All done! Except you're not. If you're wrapping VectorLike
objects into this thing, you now have a new problem. You have to begin unwrapping them whenever you want to send them out of your API, or you can start handing out VectorWrapper
objects, but you have to start checking to see if incoming VectorLike
objects are already instances of VectorWrapper
so you don't wrap them twice! Hmm... Not so perfect, after all.
This is when you start thinking arrogantly and make the mandate, "in order to use my library, you WILL use MY library's vector class!" All this does is shift the complexity of juggling these types to your library's users. Plus, it adds yet another vector class floating around in the JavaScript run-time. No one is happy with this.
"But, wait!" Some clever programmer interjects, "you're going about this all wrong! Here, look!"
// Just make all your vector math into functions instead! =)
function length(op: VectorLike): number { /* length stuff */ }
function add(op1: VectorLike, op2: VectorLike): VectorLike { /* add things, return op1 */ }
function scale(op: VectorLike, scalar: number): VectorLike { /* scale stuff, return op */ }
Wow, look at that! Behavior that works with VectorLike
and doesn't need any additional abstraction! What this clever programmer stumbled upon is something called procedural programming. But you might be seeing another problem:
import { add, scale } from 'vector-math';
let v1 = { x: 2, y: 4 };
let v2 = { x: 6, y: 8 };
let v3 = { x: 10, y: 12 };
let acc = { x: 0, y: 0 };
// Get the average of three vectors.
let avg = scale(add(add(add(acc, v1), v2), v3), 1 / 3);
That's quite the mess! Not to mention you have to start importing these vector math procedures where ever you want to use them in your code, polluting your scopes with functions floating all over the place. Unfortunately, in this case our behavior is too far separated from the structures they're made to operate on. Its uncomfortable and unwieldy, and why we moved away from it long ago.
So, while TypeScript solves the problem of structure with interfaces, it is far from graceful when dealing with the behavior of these structures.
But wait a minute! Maybe our procedural programming friend is on to something. The first parameter of all those vector math functions all share the same structure...
If you have a look at the Wikipedia page on Extension Methods, you'll see a quote right at the top by the principal developer of C#'s compiler: "Extension methods certainly are not object-oriented."
And he is right, and you can see this in the way C#'s extension methods are declared. Let's do that with our vector math procedures for comparison, real quick.
public static class VectorMathExt {
public static Double Length(this IVector op1, IVector op2) { /* length stuff */ }
public static IVector Add(this IVector op1, IVector op2) { /* add stuff, return op1 */ }
public static IVector Scale(this IVector op1, double scalar) { /* scale stuff, return op1 */ }
}
Now, with these extensions in scope, where ever we have an IVector
object, we can call these procedural functions. As extensions, the compiler marries this behavior to any IVector
making it so we can call vec.Add(...)
instead of having to use VectorMathExt.Add(vec, ...)
. Since this is all handled for us by the compiler, we also only have to do a single import statement, and since they're namespaced into VectorMathExt
, the scope pollution is minimal. But, once they are in-scope, the compiler handles the rest, replacing our nice dot-operator calls to the extension methods with the more unwieldy static class calls.
You might have noticed a while ago I keep bolding a couple of words when they're near each other: structure and behavior. That's because, deep down, this is how programs are divided up. You cannot have one without the other and still have a working program. There are many ideas on how these two concepts should inter-relate. Object-oriented programming has the structure owning the behavior: the familiar class. Functional programming has the behavior owning the structure: data lives in the stack (as much as possible).
In between, you have procedural programming, where structure and behavior do not own each other; they stay separate. Instead, structure and behavior infer each other. A procedural programmer's job is to find structures and behaviors that agree with each other and use them together, but, as we saw, this is very tedious. Now, if there were a way to map procedural functions on to compatible data structures automatically, this would be a great relief for procedural programming in JavaScript.
Basically, extensions take procedural programming and give it that object-oriented flavor we crave, and it does all this without destructively changing the underlying types. That C# IVector
interface is the same as our TypeScript VectorLike
interface, just an x
property and a y
property, with or without extension methods.
But many of you already know how extensions work. It's the fact that this is compiler magic and these extension methods cannot be laid upon the JavaScript world neatly. These extension methods vanish when they're not used in a method call! They won't act like they have these behaviors if they're passed into different scopes that lack these extensions! It would be weird!
The idea of call-site rewriting seems strange to people coming out of JavaScript, and that's largely because they're coming from an interpreted language; JavaScript doesn't have a compiler. To a JavaScript developer, there exists only 'run-time' and anything else is sorcery and witchcraft.
However, just by virtue of using TypeScript, you must deal with 'compile-time' abstractions. I would bet that everyone here stumbled on someVariable instanceof MyInterface
at one point or another and thought for a moment that TypeScript's compiler had a bug in it. But you figured it out; you understand now that 'types' are a compile-time thing and 'values' are a run-time thing, and there is a division between them.
And that is what people will learn to understand with extensions. Extensions are another one of those compile-time abstractions that don't exist in the same way in run-time.
Keeping that in mind, it would be a terrible idea to just start piling extensions onto everything. C# made the smart choice of making extensions opt-in; in order to use them, you must bring them into scope with the using
directive. Other languages which have features similar to extensions, like Rust's traits, are also opt-in and strict about it. Being opt-in, we also get the advantage that adding extensions to the compiler will not break existing code.
It should be little different for TypeScript, especially if it will take some time for programmers to adapt to the concept.
TypeScript provides a means of conjuring structure to help cope with that structural type-system, the interface
and type
keywords. Unfortunately, it has no simple means of joining behavior to these structures. This is what extensions provide and the problem they solve in TypeScript.
Ponder the following for a moment: Interfaces describe the structure of a type. Extensions describe the behavior of a structure.
It's starting to sound a lot like extensions are the missing compliment to interfaces. Interfaces cannot have behavior; they can define space for behavior, but that isn't the same thing as having it. Where-as extensions cannot have structure, but they can define what particular structures are capable of doing. It's getting harder and harder to see how you can have one of the features but not the other.
As I see it, we have a feature that is only half-implemented in TypeScript. We got interfaces that poof structure into existence; where is the thing that poofs behavior into existence to go with it? It's hard to argue that extensions do not strongly compliment TypeScript's structural type-system. It would make coping with many of the challenges that exist in the domain of JavaScript so much easier.
That being said, I have been working on a complete proposal, but I think I'll wait to see if the mood changes, after everyone has digested this, before I finish it.
@JHawkley please don't confuse the issue - it's not that extension methods aren't awesome and desirable. Everyone who uses C# recognizes their value. It's that they cannot[1] (cannot [2]) be added to the language in a way that actually works, for any suitable definition of "works". See https://github.com/Microsoft/TypeScript/issues/9#issuecomment-74302592
[1]: SRSLY CANNOT [2]: can't even
@RyanCavanaugh I actually kind of found your challenge course kind of funny, at least how you began it. Thing is: C# can't run that course with its extensions, and it is C# and it does have the CLR.
In C#, having an extension mapped to a type does not actually change the underlying type. If an object didn't match an interface before it had an extension, it still won't after. The compiler would helpfully inform you of the type mismatch. Extensions (at least how C# implements them) do not change types; they only associate free-floating procedures to types, making calling those procedures more convenient.
Fundamentally, your challenge course is confusing two concepts: extensions vs monkey-patches.
The core difference is one virtually extends an object in compile-time and the other ACTUALLY extends it in run-time. Your challenge course is making the assumption that extensions would need to work the same way in both domains, and if they don't, they fail and are therefore unworkable as a feature.
But extensions are not monkey-patches. There are other proposals for this kind of feature elsewhere. Extensions solve a different problem from monkey-patches, and so must be given different treatment and have different expectations.
They do work in TypeScript, just not in the same way as a monkey-patch. Also, the extensions that TypeScript would produce would also work fine in JavaScript; the popular library UnderscoreJS is an example of what a TypeScript library built on extensions would compile into. Perfectly usable! (In fact, a prime candidate for having its definition files re-written to support extensions.)
Also, as an aside: the only language I currently know of that could actually run your challenge course is Scala, with its implicits. Those are, uhh... really wild, though. ScalaJS exists and it does manage to bring the feature to JavaScript, but I am certain that the structures it compiles into are probably unsuitable for use in bare JavaScript, though.
Actually, @RyanCavanaugh, I think I see what you were saying. Like a lot of people, I got caught up in the whole idea of C#-style extension methods when your suggestion clearly states "adding members to an existing type's prototype"; that's obviously a case for run-time monkey-patching. And that definitely wouldn't work, especially now that we have union and intersection types to deal with. Although compile-time abstractions were discussed thoroughly, it wasn't really what the suggestion was originally about.
But, now I'm not sure what to do. I still think compile-time abstractions for associating behavior to types does work for TypeScript and has great merit for both it and JavaScript. It feels like I should create a new issue (or find an old one) that pertains specifically to compile-time extension methods, but I am pretty certain that, by now, the issue would be immediately marked a duplicate and I'd be sent right back here. I mean, this IS issue number 9. It's an old, old discussion...
But, I've expended considerable energy on this already, and I think I laid the case for compile-time extension methods as well as I could, where it was mostly discussed. At this point, either people understand the difference and see that it works or the idea dies here. I hope it doesn't, but TypeScript will continue to be a great language all the same.
Again, it would be awesome to have that feature. It's super handy. If we could wave a magic wand ✨ and make extension methods happen, we most likely would. There's no question about the desirability of the feature.
The constraint of compiling to simple JS simply makes it impossible to implement. If someone somehow came up with a way to introduce that behavior (in terms of JS emit) while still meeting our other design constraints, I think it'd be something we were really excited about. But the fact that five years later no one has come up with anything, despite everyone wanting it and wishing it existed, strongly implies that it cannot be done.
I really agree with you, @RyanCavanaugh!
I think something like binding+pipelining will work without the typing problems because the syntax is explicit.
@RyanCavanaugh I still don't understand why it can't be simple syntactic sugar. All extensions are simply a static method/function that takes this as the first property. Why can't TSC simply rewrite the function insanity for us?
interface IEnumerable<T> {
getEnumerator():IEnumerator<T>
}
// Extensions:
export function where<T>(this a:IEnumerable<T>, p:Predicate<T> ):IEnumerable<T>
{ /*... */ }
export function select<T,TResult>(this a:IEnumerable<T>, s:Selector<T,TResult>):IEnumerable<TResult>
{ /*... */ }
import {* as Linq} from "Linq";
function filter<T,TResult>(source:IEnumerable<T>, p:Predicate<T>, s:Selector<T>)
{
return source.where(p).select(s);
}
... results in:
function filter(source, p, s)
{
return select(where(source,p),s);
}
As always, I understand that there could be collisions if the existing objects had a .where
or .select
member. But: 1) In this case, the intention is clear because of the restrictive interface. 2) The compiler should be able to detect the collision and warn for both the cases where the extension itself is breaking the rule, or being broken when applied to the object being extended. 3) Also, the compiler should be able to identify if there is a difference in signatures between the two and even potentially create a virtual overload because of it.
@RyanCavanaugh And again, I understand that implementing extensions is probably going to break things in JS but it could be done in a restrictive way first pass where everything has to be in order for it to work right.
Here is a non-exhaustive list of problems with this approach, as already described at https://github.com/Microsoft/TypeScript/issues/9#issuecomment-74302592
any
? We'll have no idea which method to rewrite to, let alone whether we should at all. A slight change to a .d.ts file that you don't even own might cause an expression to become any
and break your program - super bad. How would you even diagnose this? It just looks like a compiler bug. You pulled some new definition files from DefinitelyTyped and now your program stops working at runtime? No.bind
invocation, but even this isn't the same (reference identity stops working, for example). Compound this with the previous bullet point.foo
as an extension method to bar
and pass a bar
to a function that expects { foo(): string }
? We'd have to disallow this because we can't just go rewrite the inside of the function. But now it's completely opaque why you can invoke a method, but you can't pass it to a function that wants to invoke the method on your behalf. It just looks like a compiler bug.A & B
that has an extension method foo
for both A
and B
with the same name, but different implementations for A.foo
and B.foo
? I guess you just can't invoke it, but that breaks the contract for intersection types -- now A & B
is no longer a substitute for A
nor B
. Technically we would just pick the first method we found and invoke it, but now your program's behavior would be dependent on the compiler's internal ordering of the constituents of intersection types, which is not at all specified (would the type B & A
now need to be distinct from A & B
?)A
instead of a B
due to resolution changes might result in a different method being invoked at runtime and cause a previously-working program to fail (or worse, corrupt data)@RyanCavanaugh all valid points that I'm aware of. Hence why I'm suggesting it may require a heavy amount of restrictions to work. Compile time collision detection would be essential, and not optional.
any
as the type in an extension's this
parameter? Compiler disallows.any
could be warned against or simply ignored. Yes, this might look funny. But I would suggest a compile warning by default if an extension is imported and the any
value has it applied. Again, extensions would be a 'special' feature. You'd need to then have a compiler flag to say "ignoreExtensionAny".bar["foo"] !== undefined
implying "foo" in bar
are now broken, unless we would want to somehow rewrite the in
operator in certain cases (how??). It's not clear how you could do this in a way that doesn't fundamentally change the runtime behavior of JS in many cases.const x = "foo"; bar[x]();
being identical to bar.foo();
are now broken, unless we want to start doing aggressive constant folding, but even that wouldn't be sufficient. It's hard to imagine keeping a straight face on the "all JS is TS" promise here.Applying an extension to a value that is of any could be warned against or simply ignored.
This is circular logic. How do you know that an invocation of a method of an any
expression is an extension method call rather than a normal call? You don't, because it's any
.
Yeah, like # 8 is great example. But I'm suggesting that those are not cases where extensions be valid. It would have to be a function, and it would have to be dot notation.
You don't, because it's any.
any
does throw a wrench in the gears.
Maybe the use of any
is somehow disallowed in modules that use extensions.
I guess ultimately, I don't see it as impossible. It's just problematic or difficult to get right.
I mean, it's possible as long as you're willing to accept a feature that is outright dangerous to use, has non-working scenarios that look like compiler bugs, restricts the use of many common JS idioms, requires type-directed emit, doesn't interop with plain JS at all, and prevents the TS type system from ever changing again.
It's also possible to wire your house with uninsulated 36 gauge wire and bubble gum as long as you're willing to accept it sometimes catching fire. :wink:
@RyanCavanaugh could you provide a sort of similarity in terms of how dangerous extension methods are in C# vs TS? - I mean we're all talking about extension methods, essentially because they're in C#, and we'd like to see them in TS...
...brb, need to go rewire my house quickly...I can smell burning gum!
@RyanCavanaugh at your last comment: I have to respect consistency. So I agree, adding a feature that creates confusion is bad. Thank you for the in-depth reply.
@series0ne if I may help out here and summarize...
Just the notion of any
is enough to create a serious issue.
There's just too much opportunity for collision. And as I chatted with Ryan about, you could solve these headaches, but it would create different headaches and inconsistencies that make it not worth it.
There's no danger in C# because C# a) resolves all call sites at compile-time, b) uses a nominal type system, c) uses an explicit opt-in mechanism to provide dynamic method calls (in which extension methods simply don't work), and d) doesn't allow method references in typeless contexts. Consider this C# code:
IEnumerable<int> enumer = null;
List<int> list = null;
Func<int> x = enumer.Count;
int y = list.Count;
The IL for the last two statements there is very different!
We also have to consider code like this
var z1 = enumer.Count; // Error
var z2 = list.Select; // Error
C# rightly errors on these, but TS really can't - that's valid JS and we have to emit something.
@RyanCavanaugh, maybe you've already done so, but I think this topic is worthy of a blog post. :P "Why JS/TS cannot have extensions"
Essentially it seems then, that we will simply have to stick to statically bound method calls in TypeScript, the same way we would have done it with C# before extension methods :(
The bind operator https://github.com/tc39/proposal-bind-operator is very promising and would accomplish nearly all the extension method scenarios quite cleanly. So keep some hope :wink:
😯 @RyanCavanaugh you would not believe the post I'm writing right now...which, without seeing the bind operator proposal, is virtually identical (great minds...?) here goes...
Essentially the only way I can see this working is to introduce an operator which instructs the TypeScript compiler, this is an extension method call! This borrows from C++
Example
class StringExtensions {
public static isUpper(this string value): boolean {
return value.toUpperCase() === value;
}
}
var x: string = "Hello World";
x->isUpper();
//or
x::isUpper();
Even if isUpper
was already a member of String.prototype
, TypeScript would know to use the extension method, not the prototype bound method, unless the operator changed.
In terms of dealing with things like union or intersection types, I think the only solution would be to make the extension method invariant; for example
var x: string | number = 0;
x::isUpper(); // Error, cannot call this because the extension method expects type "string"
Thus, you would require an overload to allow this behavior.
Again, I did NOT read about the bind operator before writing this, and it's just escaped my noodle, so there are likely some gaping holes!
P.S. probably want to go with ::
, and not ->
because TS already has =>
and =>
/->
look too similar.
Yeah, the bind operator would go a long way toward giving us extension-like behavior. It still isn't monkey-patching, won't fix types like you might want, and has its own challenges for integrating into TypeScript, but not impossible challenges.
TypeScript would have to start tracking the type of this
in a function more aggressively, for instance. We already have that ability to explicitly specify its type with the fake this
parameter, but it would have to be expanded to automatically infer the type of this
for functions pulled from class prototypes and the like.
Anyways, I still think TypeScript can benefit from a compile-time abstraction for associating behavior to types, especially since it has one for structure. Many current JavaScript libraries are created in a manner that would work well with it and they wouldn't be helped by the bind operator. Also, it would reduce the compulsion to duplicate function implementations in separate classes or use unintuitive forms of inheritance, all just to get the nicer feeling method-call syntax. I've done that before...
I think I may submit my current proposal under the terminology the programming language D uses, 'Uniform Function Call Syntax', and try to keep the use of the words 'extension' or 'extends' to a minimum.
Then maybe the Scala extensions would be help this kind of problem. In Scala, I can define a class which will provide methods and properties to an instance. So, in resume:
extension class MyExtension(target: any)
{
printHello() {
console.log("DO MY CODE");
}
}
var something: any = ...;
something.printHello();
could be transformed to this equivalent:
class MyExtension(target: any)
{
printHello() {
console.log("DO MY CODE");
}
}
var something: any = ...;
var something$MyExtension = new MyExtension(something);
something$MyExtension.printHello();
and then to the equivalent javascript code. Thus, In the end:
@danfma I don't see how that's meaningfully different from the dozens of upthread suggestions for similar rewriting of call sites, nor how it addresses the serious problems with that approach.
@RyanCavanaugh, that is just another suggestion like many others above :D
But I still don't get what are the problems with the previous ideas. For example:
let x = 10;
extension function plusOne(this self: number): number {
if (typeof self !== "number") throw new TypeError(); // or anything else
return self + 1;
}
let y = 10.plusOne();
Generated javascript:
var x = 10;
function plusOne(self) {
if (typeof self !== "number") throw new TypeError();
return self + 1;
}
var y = plusOne(x);
import { plusOne as plus1 } from "some/place"
;Then, please, can you point me what you see that I can't see as a problem?
About the extension classes, it has the advantage to allow you to extend with more than one method, but it will make the compiler life hard when having two extension classes with the same function name. One solution for that, is to not resolve automatically when that occurs, so if a name collides, the compiler can raise an error, and let the user resolve, by manually creating the extension by hand, for example.
All of the problems raised at https://github.com/Microsoft/TypeScript/issues/9#issuecomment-74302592 and https://github.com/Microsoft/TypeScript/issues/9#issuecomment-263978824 apply
ok, I will check!
@RyanCavanaugh, I got your points, but I still disagree in some points. Javascript itself is a dangerous language because you can do a lot of things to bug your own code and like the TypeScript team always talks 'TypeScript is JavaScript', so it inherits all the JS problems.
So when you say It's also possible to wire your house with uninsulated 36 gauge wire and bubble gum as long as you're willing to accept it sometimes catching fire
, you are right, but you already accepted that too! :D
What I want to say is that even in C# you can have problems by using dynamic
or Reflection, so I don't see the difference here.
Anyway, I will stop here and thank you for the long list provided before. 👍
👍
An alternative is to include a forward declaration of the methods you are extending in the class, then modifying the prototype to extend them. For example:
class Greeter {
greeting: string;
say: Function;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
Greeter.prototype.say = function(msg:string) {
alert(msg);
}
let greeter = new Greeter("world");
greeter.say(greeter.greet()); // alerts "Hello, world"
Hope it helps
Honestly, I'd love to see a move away from internal methods at all... some day? Monkey-patching and static helpers only go so far. CLOS and Dylan are good examples of adding methods to classes at any time -- without monkey-patching, and they're lexically scoped. The problem with C# extension methods is that they are statically dispatched, just some syntactic sugar.
If every method was external, you could do something like this:
var _capitalize = capitalize || new Map();
String.addMethod(_capitalize, function() { return this.slice(0, 1).toUpperCase() + this.slice(1); });
"foo".invoke(_capitalize);
// Implementation
Object.prototype.addMethod = function(method, func) { method.set(this, func); }
Object.prototype.invoke = function(method, args) { return method.get(this.constructor).apply(this); };
// Syntax thoughts, where extend creates a locally scoped external function.
extend String { capitalize() { ... } }
"foo".capitalize();
Just some thoughts :) Mike
Some of the examples @RyanCavanaugh wrote on why rewriting call-sites wouldn't work for JS targets, actually works in Haxe (similar to TypeScript and can transpile to JS and others).
See here for one of the examples, in Haxe. Click the build button to see the trace outputs and the JS transpiled output.
Some of his examples don't work, but the Haxe compiler actually manages to catch some of those errors ("x has no method getArea"). However, using the any type (Dynamic in Haxe), will definitely not work, but haxe coders know why they don't work, because in haxe docs, it was stated that we should avoid using Dynamic as it turns off certain type checking features.
To make most of Ryan's examples work in haxe (haven't tried all of them), simply type cast or add type hinting so the compiler knows what to do. Of course, if someone convinced the compiler that a chicken is a duck, but it doesn't quack at runtime, then the problem isn't that the feature is impossible. It is the person who convinced the compiler that the chicken is a duck.
For union types (Square and Rectangle; where one has instance method getArea and the other a static extension getArea), the compiler should show an error because getArea does not really exist for one of them. It should be made that once the type is checked (through an if guard or something), then getArea should work (since within the if statement, the compiler can tell if its a square or rectangle).
Static extension methods can only work if the compiler is able to know the type as it is a compile-time feature. Programmers shouldn't expect something untyped/not properly typed/unverified to take advantage of a compile-time feature that requires type to be known. I think it is possible to implement and that it is also reasonable to expect it not to work if the compiler is unable to infer the type.
Allow a declarative way of adding members to an existing type's prototype
Example: