-
Notifications
You must be signed in to change notification settings - Fork 12.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Suggestion: Extension methods #9
Comments
Highly suggested 👍 The original issue at codeplex had pretty much lots of votes: |
👍 |
1 similar comment
👍 |
Perhaps this can be used in an ambient context as well : declare module foo{
class A{}
extension class A{}
} |
|
A couple of questions:
Having considered |
While the naive implementation of extension methods is probably like this: extension class Shape { // Syntax??
getArea() { return /* ... */; }
}
console.log(x.getArea()); // OK Compiling to: // js
var _Shape = {
getArea:function() { return /* ... */; }
}
console.log(_Shape.getArea.call(x)); I think due to first-class functions in JS there is a problem with this approach: extension class Shape { // Syntax??
getArea() { return /* ... */; }
}
var x = new Shape();
var y = new Shape();
console.log(x.getArea); // Function or undefined?
x.getArea.foo = "bar";
console.log(y.getArea.foo); // "bar" or undefined? If extension methods are to be implemented, there would probably have to be restirictions on accessing/changing their properties. Considering "class" syntax extension class Shape { // Syntax??
getArea() { return /* ... */; }
} In my opinion, one of the common uses of extension methods is to make them work on interfaces, not just classes, so you could do something along the lines of interface IPoint {
x:number;
y:number;
}
class Point implements IPoint {
constructor(public x,public y) {
// ...
}
}
var p1:Point = new Point(0, 50);
var p2:IPoint = { x:32, y:32 };
console.log(p1.getLength()); // extension method
console.log(p2.getLength()); // same extension method If possible, I think C#-like extension methods would work best: extension function getLength(this p:IPoint):number {
return Math.sqrt(p.x*p.x + p.y*p.y);
}
// another extension method with the same name but for different class
extension function getLength(this s:string):number {
return s.length;
}
console.log(getLength); // compiler error
console.log(p1.getLength); // Function or undefined or compiler error?
console.log(p1.getLength.foo); // compiler error
p1.getLength.foo = "bar"; // compiler error Compiling to: // js
function _IPoint$getLength() {
return Math.sqrt(this.x*this.x + this.y*this.y);
}
function _string$getLength() {
return this.length;
}
console.log(_IPoint$getLength.call(p1)); // OK
console.log(_IPoint$getLength.call(p2)); // OK |
Hmm, I thought that this proposal was just to extend prototypes. class A {
foo() {
}
}
extending A {
bar() {
}
} Compiling to: var A = (function () {
function A() {
}
A.prototype.foo = function () {
};
return A;
})();
A.prototype.bar = function () {
}; |
I don't think extension methods should be implemented by adding a function to the prototype! For one thing extending the prototype of built in types, is considered dangerous, 3rd party libraries may also add a method with the same name to the prototype, causing the behavior to depend on which JS file was loaded last, or worse you might overwrite the existing built-in methods! And for another, there's no way to have different method overloads for the extension methods defined in different modules! |
I still agree with @saschanaz . There are issues surely but for :
this would be caught by name collision in the lib's
I don't expect it there to be any codegen for ambient declarations. PS: If you ambiently say something is a class TypeScript already assumes it has a prototype. e.g. declare class Foo{}
console.log(Foo.prototype); // Compiles fine So adding to the ambient prototype is consistent with what already exists. Perhaps examples of your concerns (if still there) would help. |
This is only true if there is .d.ts metadata for every 3rd party library (which is not) and only when .ts files are being compiled together (ie. in the same project). Besides there are lots of .js libraries as part of server-side environment which are being injected into dynamic pages at runtime (take asp.net server controls for example)
Ambient class declaration does not have implementation, which is not the case for extension methods, unless extension methods for ambient class declarations are being erased in the compiled .js (which I believe is counterproductive to it's purpose), the program might break at runtime! And how about interfaces? To extend an interface new methods has to be added to the interface, but changing the interface by adding new methods would break compatibility with existing consumers! Take I believe the real advantage of extension methods is being loosely coupled with the existing type, e.g.
The above example is supposed to add linq-like join operator for arrays, however having altered the prototype, we have lost the built-in join method. Having extension methods separated in a concrete different method actually benefits better method overloading. There's also a big benefit with generic extension methods which is reducing the need for a common base class. public static bool In<T>(this T obj, params T[] source)
{
return new HashSet<T>(source).Contains(obj);
} Which you can simply use: var o = 1;
o.In(1, 2, 3, 4, 5); /// true |
Great discussion so far. There's a key decision point here on this feature, which is how the emitted code would work. I see two options based on what's been discussed so far Example code I'll use class Square { /* ... */ }
extension function getArea(this: Square) { // First param must be 'this'
return this.width * this.height;
}
var x = new Square(10, 10), y: any = x;
console.log(x.getArea());
console.log(y.getArea()); Add to .prototype/* Regular emit for 'Square' here */
Square.prototype.getArea = function() {
return this.width * this.height;
}
var x = new Square(10, 10), y = x;
console.log(x.getArea());
console.log(y.getArea()); // Succeeds Pros
Cons
Rewrite call sites/* Regular emit for 'Square' here */
__Square_getArea = function() {
return this.width * this.height;
}
var x = new Square(10, 10), y = x;
console.log(__Square_getArea.call(x));
console.log(y.getArea()); // Fails Pros
Cons
|
@RyanCavanaugh I would alter the example code, to use the type parameter for the extension method instead of dynamic binding of class Square { /* ... */ }
extension function getArea(this square: Square) { // First param must be 'this'
return square.width * square.height;
}
var x = new Square(10, 10);
console.log(x.getArea()); Which would translate to this: function getArea(square) {
return square.width * square.height;
}
var x = new Square(10, 10);
console.log(getArea(x)); So that the function behave like a static helper method and it's valid to call it non-instance way. However still fails when strong type information is not present. You can call the extension function explicitly: var x = new Square(10, 10), y:any = x;
console.log(x.getArea()); // Succeeds
console.log(getArea(y)); // Succeeds |
I like the suggestion of @KamyarNazeri, this can be used on an enum (or an union in the feature) too. This code is also faster than a prototype call or a Would the |
@ivogabe +1 for mentioning enum, I would like to also mention that using static functions, you can call methods on objects that are null, e.g. |
How about this? class Square { /* ... */ }
function getArea() joins Square {
return this.width * this.height;
}
var x = new Square(10, 10);
console.log(x.getArea()); Translates to: /* Regular emit for 'Square' here */
__Square_getArea = function(_this) {
return _this.width * _this.height;
}
var x = new Square(10, 10);
console.log(__Square_getArea(x)); PS: Or without renaming, as @KamyarNazeri did. I like the non-instance way. function getArea(_this) {
return _this.width * _this.height;
}
var x = new Square(10, 10);
console.log(getArea(x)); |
I want to emphasize the danger of the "rewrite call sites" approach: class Square { /*... */ }
/* Extension method getArea, syntax irrelevant */
function process(callback: (n: Square) => void) {
/* invoke callback on a variety of Squares */
}
process((n) => {
n.getArea(); // Succeeds
});
process(((n) => {
n.getArea(); // No compile error, fails at runtime
}));
var x = new Square(), y: any;
var arr1 = [x];
arr1[0].getArea(); // Succeeds
var arr2 = [x, y];
arr2[0].getArea(); // No compile error, fails at runtime There are very strong reasons TypeScript avoids rewriting code based on type information (see https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals). No one wants to get in a situation where adding a non-observed element to an array or adding extra parentheses around a function could causes program to start to fail at runtime. |
@RyanCavanaugh Seriously I don't understand why by adding extra parenthesis around a lambda function, the runtime infers variable var a = function (n: number): void { }
var b = (a); // here b accepts number! As for the latter example, I believe no-one expects an extension method to work when boxing variable to another type (here |
We explicitly decided parenthesizing an expression should be the way to opt out of contextual typing or else you were always at the mercy of the inference algorithm even if it was wrong/incomplete. |
Maybe we can choose separate syntax to call those functions. class Square {
foo() {
}
}
function getArea() joins Square {
return this.width * this.height;
}
var x = new Square(10, 10);
var y: any = x;
console.log(x calls getArea()); // Succeeds
console.log(y calls getArea()); // Succeeds both in compiling and in runtime.
console.log(x.getArea()); // Fails, this is only for functions in prototype
console.log(x calls foo()); // Fails, this is only for extension functions PS: Fixed to allow |
This turned to be more complicated that it looked like! Having rewrite call sites comes handy when coding, however I agree that the code might break at runtime! I've seen lots of javascript developers debugging their code in the browser developing tools which also fails using rewrite call sites.
Maybe we could consider Java 8 default methods (or as they are often called defender methods) to extend an existing interface. Of course default methods in an interface could translate back to prototype in the emitted js code: interface ISquare {
width: number;
height: number;
default getArea(): number {
return this.width * this.height;
}
}
class Square implements ISquare {
constructor(public width: number, public height: number) {
}
// no need to implement getArea here
}
var x = new Square(10, 10), y: any = x;
console.log(x.getArea()); // Succeeds
console.log(y.getArea()); // Succeeds which would compile to: var Square = (function () {
function Square(width, height) {
this.width = width;
this.height = height;
}
Square.prototype.getArea = function () {
return this.width * this.height;
};
return Square;
})(); |
This interface looks more like an abstract class. #6 |
Only a class might already inherit another base class, I think the real benefit with interface extension comes with multiple inheritance simulation. |
The difference is that interfaces in TypeScript can be used without classes. How would the following code compile? interface ISquare {
width: number;
height: number;
default getArea(): number {
return this.width * this.height;
}
}
var square: ISquare = {
width: 100,
height: 50
};
console.log(square.getArea);
Maybe call the function like this: (since this syntax already exists) getArea(square);
// instead of
square calls getArea(); |
Chaining would become uncomfortable in that case. Example: var num = new Number(0);
function plus(input: number) joins Number {
return this.valueOf() + input;
}
num.plus(3).plus(3).plus(3);
num calls plus(3) calls plus(3) calls plus(3); // Well... not very good to see. Hmm versus plus(plus(plus(num, 3), 3), 3); ...while the rewritten JavaScript will be anyway Edited: num->plus(3); // Syntax that looks like the C++ one but for extensions? or num<-plus(3); // function 'plus' joins to 'num'. |
Yea well, object literals are not the only problem: interface A {
default void foo() {
console.log("A");
}
}
interface B {
default void foo() {
console.log("B");
}
}
class Bar implements A, B {
default void foo() { // required for conflict resolution
console.log("Bar");
}
}
var o = new Bar();
A o1 = o;
B o2 = o;
o.foo(); // Bar
o1.foo; // probably want A
o2.foo; // probably want B From the client code perspective, default methods are just ordinary methods, so in case of the simple example with one class that implements an interface with a default method, prototype seems to be fine, however most of the time client code that invokes the default method will have to invoke the method at the call site. same old story. 💤 |
For the record, here's an example of runtime exceptions in C#: Personally I'm strongly against any silent prototype modifications - changes to any existing prototype (everything except initial class-defining code generated by TS compiler) should always be explicit, having programmer manually type "class.prototype" to prevent at least some part of the errors. Extension methods are nothing more than syntax sugar for method calls (at least that's how I see it and how they are implemented in C#). While certainly useful, they are not necessary. |
In both C# and Scala an instance method will always win out in terms of overload resolution so I think that's a good approach. The problem with applying that approach in TypeScript is not static checking but rather that it is impossible to do it without resorting to type directed emit, which is not an option. |
I see the type-directed-emit topic comes up with async/awat, but other than that is it not used anywhere else? Sorry, I don't mean to keep this topic alive forever :) Side note -- taking your extension methods comments to heart, I've come up with this for my language:
Again, it's not going to be fast, but at least it's short. |
@mikeaustin I like it, that's a very clean emit, I'm curious to see how your language progresses. With respect to type directed in async methods, IIRC it only rears its ugly head when you don't use type inference for return types and don't install a global Promise polyfill, as it only considers the type when provided, defaulting back to the global I couldn't care less about the extra methods available on Bluebird Promises and I never specify the return type of So I believe the issue is easily dodged, but I may be wrong and it may be affecting me in some other way that I'm not aware of. |
Sorry for spamming the TypeScript list, but I made a slight change in my extension methods to support traits/mixins that might be interesting. When extension methods were in local scope, there was no good way (aka not using eval()) to dynamically add methods. So, I encapsulated them in a _methods lexically scoped object:
I can probably simplify extend() a bit more. The language this is targetting: Impulse-JS. |
With the amount of library authoring going on and what seems like increased adoption of TS, now seems like a great time to dust this idea off and see if it's worth dedicating resources to! Again, biggest thing is for libraries. I already find myself very badly wishing for some kind of idiomatic way to sprinkle some extensions around. ✏️ |
Seems people aren't reading the thread. |
* Class static block (microsoft#9) * Add types factory and parser * Add some case * Make class static block as a container * Update cases * Add visitor * Add emitter and more compile target * Check boundary of break and continue * Add basic transformer * Fix emit behavior * Add more tests * Add friend tests * Update baseline * Fix cr issues * Accept baseline * Add decorator and modifier check * Add functional boundary check * Fix conflict * Fix computed prop name within context * Add more tests * Update baseline * Avoid invalid test baseline * Support use before initialize check * wip * Fix class static block context * Fix checks * Fix missing case * Improve assert message * Accept baseline * Avoid new context * Update diagnostic message * Fix name collision * Fix targets * Avoid unnecessary files * Add more case * Add more test cases * Fix strict mode function declaration * Avoid private fields initializer if no private identifier references * Avoid private fields and add more test case * Add more case * Add tests and support for related services functionality * Fix this reference in static block * Split parser diagnostic and binder diagnostic Co-authored-by: Ron Buckton <ron.buckton@microsoft.com>
I would personally be happy if Typescript had extension methods that used a distinct call syntax vs. normal method invocations. For instance: function Date#toProto(): Timestamp { ... }
function usage() {
const proto: Timestamp = new Date()#toProto(); // instead of toDateProto(new Date())
} This would allow
I suspect it would avoid some of the complications from #9 (comment). Making extension functions callable from JavaScript might require some attention, however. |
@reddaly out of scope for TypeScript. See https://github.com/tc39/proposal-bind-operator though |
What makes the #9 (comment) suggestion out of scope for TypeScript? Extension methods seem like they should be resolved at compile-time based on typing information that's out of scope for ECMAScript. I'm not sure how ECMAScript could possibly provide the same level of utility through the bind operator. function Foo::toProto(): FooProto { ... }
function Bar::toProto(): BarProto { ... }
(new Foo())::toProto() // resolves to first method
(new Bar())::toProto() // resolves to second method edit: Reading into https://github.com/tc39/proposal-bind-operator, it seems the "::" would help a lot. However, in the above example, multiple "toProto" methods probably could not be imported into the same TypeScript file - they would have to be aliased. import {toProto as toFooProto } from './foo-extensions'
import {toProto as toBarProto } from './foo-extensions'
(new Foo())::toFooProto()
(new Bar())::toBarProto() This might seem like a small detail, but the namespacing provided by a method receiver is helpful for keeping method names concise. |
That's type-directed emit. See https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals#non-goals point 4:
|
Thanks. It seems the more active proposal is https://github.com/tc39/proposal-extensions. I asked here about the status of that proposal advancing to stage 2. Presumably TypeScript is not interested in introducing its own bind operator before there is consensus to add the operator to JavaScript. |
Allow a declarative way of adding members to an existing type's prototype
Example:
The text was updated successfully, but these errors were encountered: