Closed RyanCavanaugh closed 8 years ago
I agree with @sccolbert. TypeScript isn't trying to be C# or Java, it's trying to add some sanity to JavaScript. The extensions concept, while cool, is at odds with the way JavaScript classes actually work.
These two statements appear to be at odds with each other.
An extension doesn't own the class, it shouldn't be able to mess with it's prototype.
The extensions concept, while cool, is at odds with the way JavaScript classes actually work.
That's exactly how JavaScript works.
I don't really think that TypeScript should bend over backwards to cater for situations where people are happy to load dodgy scripts from third parties.
Let me rephrase: an extensions concept that requires rewriting call sites is at odds with the way javascript classes actually work, and an extensions concept that modifies the prototype is at odds with the way C#/Java programmers understand extensions.
Yes, C#/Java users want rewriting call sites, but it seems there's no good way to do that. So I still think we may need third-party way to implement really-JS-like extension methods, at least until ECMA people do something here.
interface ObjectStatic {
bindExt(extensionFn: (base: this, ...args:any[]) => any); // Still from potential third-party library
}
Object.prototype.bindExt = function (extensionFn: (base: this, ...args: any[]) => any) {
return (...args: any[]) => extensionFn(this, ...args); // ES6 Spread
}
(Burrowing this
type concept from #289)
Now there should be no dynamic script loading, while I don't like the ...args: any[]
part.
class Foo { /* */ }
function fooExtensionMethod(base: Foo, arg1: number, arg2: string) { /* */ }
var foo: Foo;
foo.bindExt(fooExtensionMethod)(arg1, arg2);
I think this will provide C#/Java-like extension method. No additional syntax, no rewriting based on type info, but still it gives extensions.
Reading this discussion I still do not understand why extension methods can't be implemented just like the static C# extension methods. I mean why couldn't the compiler could just emit invocation of the static method?
Because TypeScript is not C# and JavaScript is not the CLR.
Here's some code that people would very reasonably expect to work; none of these would be possible if we tried to extension methods with call site rewrites.
// 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());
});
The only plausible language in which we rewrote call sites would have the following restrictions:
any
, your code will break at runtime with an error you won't be able to diagnose via debugging the code itself. This will look like a compiler bug to most peopleWhen you add up all those restrictions, the only real advantage foo.extMethod()
has over extMethod(foo)
is that there's an extra dot in the extension method call. Plus, it creates a giant trap by having extension method calls on any
variables fail. That's not a good deal.
@RyanCavanaugh this is an insanely detailed explanation, thank you! When this thread started, I was sure that extension methods are a very good idea, now I believe we're better of without them (at least for now).
@RyanCavanaugh Have you looked into using a ~ proxy: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
Not sure where that proposal stands but fairly optimistic this can work:
class Rectangle {
constructor(public width: number, public height: number) {}
static foo() {
return 'foo';
}
}
Object.seal(Rectangle);
extends(Rectangle, {
static bar(){ return 'bar'; }
getArea() {
return this.width * this.height;
});
Emits
class Rectangle {
....
}
proxy_Rectangle = new Proxy(Rectangle, {
get: function(target, name){
var extension = {
bar: function() { return 'bar'; }
}
return name in extension ? extension[name] : target[name]
}
});
proxy_Rectangle_new_handler = {
get: function(target, name){
var extension = {
getArea: function() { return this.width * this.height; }
}
return name in extension ? extension[name] : target[name]
}
});
// All futur references to 'Rectangle' are wrapped by the proxy
function fn1(x: HasArea) {
console.log(x.getArea());
}
fn1(new Proxy(new Rectangle(10, 10), proxy_Rectangle_new_handler));
proxy_Rectangle.bar()
+1
At present, the primitive types can be extended as:
// Extending String prototype
interface String {
format(...params: any[]): string;
}
// using `any` so consumer can pass number or string,
// might be a better way to constraint types to
// string and number only using generic?
String.prototype.format = function (...params: any[]) {
var s = this,
i = params.length;
while (i--) {
s = s.replace(new RegExp('\\{' + i + '\\}', 'gm'), params[i]);
}
return s;
};
// Usage:
// 'Added {1} by {0} to your collection'.format(artist, title)
Would be nice to have extension
keyword fit in that kind of context as well. :)
+1
How could someone contribute for the extension classes in Typescript ?
Would be great if we could extend enums with this. Something like
enum View{
Edit,
Detail
}
View.Edit.asString()
vs what we have to do now:
View[View.Edit]
The first one is a bit more readable and flows nicer than the latter.
Note that you can add to an enum
using module
(or namespace
):
enum E {
A, B
}
module E {
export function toString(e: E): string {
return E[e];
}
}
var x = E.toString(E.A);
->
for call
.So this:
o.my()::Extension.method().rocks();
// or preferably this
o.my()->Extension.method().rocks();
desugars to:
Extension.method.prototype.call(o.my()).rocks();
There's a possible ambiguity if there were extension properties though, or methods which return extension methods. There's also the issue that using a different syntax instead of dot hurts the motivating case which is I think is transparency.
:+1:
It seems implement in TypeScript 1.7
class Shape {
// ...
}
interface Shape {
getArea()
}
var x = new Shape();
console.log(x.getArea()); // OK
Should we close this issue ?
That does not create a new method
Well this is exactly what I was looking for, so for me a YES, on closing this issue.
@Zorgatone
you can create a new method with this way
class Shape {
// ...
}
interface Shape {
getArea()
}
Shape.prototype.getArea = function(){
}
var x = new Shape();
console.log(x.getArea()); // OK
Oh, that's with prototype. I wasn't considering that.
I tried to do something similar, but without the interface trick :+1:
@WanderWang @Zorgatone You can't do that, I tried doing that in separate files using the es6 module syntax and got tons of errors. I think it only works with the legacy external modules.
Uhm then keep the issue open :D
-1 Humans cannot understand code flow using extension methods. Extension should be declarative feature.
The problem with doing this is that (as hopefully we’ve all dealt with), is it could pollute the existing code in a more complex environment. “Extensions” would simply be compiler sugar for writing utility methods that would pass the ‘this’ along.
From: Wander Wang [mailto:notifications@github.com] Sent: Friday, October 30, 2015 5:52 AM To: Microsoft/TypeScript Cc: electricessence Subject: Re: [TypeScript] Suggestion: Extension methods (#9)
@Zorgatone https://github.com/Zorgatone
you can create a new method with this way
class Shape { // ... }
interface Shape { getArea() }
Shape.prototype.getArea = function(){ }
var x = new Shape(); console.log(x.getArea()); // OK
— Reply to this email directly or view it on GitHub https://github.com/Microsoft/TypeScript/issues/9#issuecomment-152515414 .Image removed by sender.
-1 Humans cannot understand code flow using extension methods.
(-1'ing everything is not really helping)
There are countless real world use-cases where extension methods are proven to be useful.
If you are unsure about the significance of extension methods, take a look at C#.
If you don't like the way it is proposed to be implemented, please provide an alternate approach.
Extension should be declarative feature.
Yes and this is in the very first line of this proposal.
I don't think that it is declarative because we cannot get the whole story. We must find pieces of a class.
If you are building the library yourself, then you should be able to add the additional methods in the class. If you are working with someone's else library (which is most likely documented) then you can add extension methods elsewhere in your code without touching vendor/**
code. Finding the pieces to form the 'whole story' is a non-issue especially when editors are there to assist you.
Splitting the code into separate files vs. dumping everything in one place, both have pros and cons. Giving this choice to the developer however they want to consume the feature (or avoid it) would be the best strategy from TypeScript team's standpoint. Any feature of any language can be misused.
:+1:
:+1:
I would suggest that Extension is only useful for telling the compiler there is an existing member for the existing class, so the code:
class A {}
extension class A {
func() : void => {
}
}
could be just compiled to:
class A{}
A.prototype.func = () : void => {
}
but such Extension is of great use to intellisense.
+1 I think the extension methods should support interfaces and be a syntactical sugar like in C#:
interface IMovable {
position: Point;
}
extension class MyExtensions {
move(this moveable: IMovable, newPoint: Point) {
...
}
}
var car: Movable = {...};
car.move(newPoint);
this will compile to:
var MyExtensions = {
move...
}
var car = {...};
MyExtensions.move(car, newPoint);
if this isn't already the case i think @ronzeidman is absolutely right :+1:
If we are really hesitant to support extension class Homu
syntax, can we just add type information from Homu.prototype.method1 = () => {}
, similar to what Salsa does (#4955)?
@ronzeidman @TrabacchinLuigi you need to read https://github.com/Microsoft/TypeScript/issues/9#issuecomment-74302592
@RyanCavanaugh The example in https://github.com/Microsoft/TypeScript/issues/9#issuecomment-74302592 is a good one. I've now read the entire thread and there were great suggestions, I like the ES7 bind operator (::) but I suggest something a bit different - introducing an "Elixir" like pipe operator "|>":
function getArea(x: Rectangle) { ... }
var myRectangle = new Rectangle();
myRectangle |> getArea();
it will make things like "extending" array functions much easier and much more readable without changing the prototype:
grouppedReduce(groupBy(mySort(myArray.map(...), ...).filter(...), ...), ...);
myArray.map(...)|>mySort(...).filter(...)|>groupBy(...)|>grouppedReduce(...);
What do you say?
We're almost certainly not taking up any new runtime operators that don't have at least some traction with the ES committee (especially when there are competing operators with similar meaning), so taking that to ESDiscuss would be the next step if you'd like to see it happen.
@RyanCavanaugh I like the bind operator too!
@RyanCavanaugh I agree, and the bind operator (of which I guess people agree on since its an ES7 suggestion) could work really well, especially if you could configure the type of "this" the function can get like in https://github.com/Microsoft/TypeScript/issues/6018 suggestion
Hey just want to express my +1 for @ronzeidman -style extension classes with a few more words. I fully realize that this is something that will differentiate TS from ES, since it's impossible to pull it off without a type system, but I also think that this is the sort of thing that leverages the type system for an amazing gain in developer experience, and it makes perfect sense for our language.
ES will obviously never get a type system which means it will never be able to get this feature either, while we can easily have and love it every day. It transpiles to ok looking JS, and to anyone thinking otherwise, well today I have to type a lot of the same stuff manually on a daily basis. Re: worries that methods won't behave as usual when deferenced or tested for existence, etc, I really don't see this a major problem, since that is rather niche usage wrt method calling. As such those can result in a compiler error and I just don't see how that can become a problem.
Aside from the obvious case for interfaces, it'd allow one to extend collection types like MyType[]
that just cannot be done otherwise sensibly. Underscore-like utility libraries will become a syntactic bliss. I imagine it'll play very nicely with this-typing for building fluent apis too.
Brief - life will be greater, and more beautiful, and so much more fantastic! We have a type system, there's nothing wrong in using it to make our lives even better. This feature relies entirely on the type system, and as such, in my opinion, is a perfect fit for TS.
Class extension would be useful for separated definition members with different access level:
class A {}
private extension class A {
func1() : void => {}
}
protected extension class A {
func2() : void => {}
}
Just wanted to give my humble suggestion, pardon my two-week TypeScript skills.
I have been reading this discussion and maybe we are trying to do something primarily designed for type-based systems. JavaScript is much less typey, so work with that (this syntax is just one way):
Given this class:
class Square {
width: number;
height: number;
}
Case 1: Does not touch the prototype:
extension class SimpleSquareExtensions<Square> {
depth: number = 0;
static scale: number = 0;
// must be static
static function getArea(this square: Square) {
// Can't access `depth` at all, not static
// Can't do this: `square.scale` - not on Square
// Can do this: `SimpleSquareExtensions.scale`
return square.width * square.height;
}
}
// JS:
SimpleSquareExtensions.scale = 0;
SimpleSquareExtensions.prototype.depth = 0;
SimpleSquareExtensions.getArea = function (square) {
return square.width * square.height;
}
This is a very basic type that does not touch the Square
prototype, so:
var sq: Square = new Square();
var area = sq.getArea();
// JS:
// var area = SimpleSquareExtensions.getArea(sq);
var anySq: any = sq;
var anyArea = anySq.getArea(); // No Intellisense for getArea()
// JS:
// var anyArea = anySq.getArea();
// Runtime error - expected
This works like C# dynamic object, I can't apply a Square
extension method to an object that might not be a square.
Also, we could, or not, permit other non-extension members on the class, such as the depth
and scale
properties. (could be methods too)
Case 2: Adds to the prototype:
extension class MoreSquareExtensions<Square> {
extension static scale: number = 0;
extension depth: number = 0;
extension static function getArea(this square: Square) {
// Can't do this: `square.depth` - static method
// Can't do this: `Square.depth` - not static field
// Can't do this: `MoreSquareExtensions.depth` - not on MoreSquareExtensions
// Can't do this: `square.scale` - static field
// Can't do this: `MoreSquareExtensions.scale` - not on MoreSquareExtensions
// Can do this: `Square.scale`
return square.width * square.height;
}
// no need for static
extension function getVolume(this square: Square) {
// Can't do this: `Square.depth` - not static field
// Can't do this: `MoreSquareExtensions.depth` - not on MoreSquareExtensions
// Can't do this: `square.scale` - static field
// Can't do this: `MoreSquareExtensions.scale` - not on MoreSquareExtensions
// Can do this: `square.depth`
// Can do this: `Square.scale`
return square.width * square.height * square.depth;
}
}
// JS:
Square.scale = 0;
Square.prototype.depth = 0;
Square.getArea = function (square) {
return square.width * square.height;
}
Square.prototype.getVolume = function () {
return this.width * this.height * this.depth;
}
Due to the extension
keyword, the developer is instructing the compiler to modify the type. The dev is responsible for resolving conflicts:
var sq: Square = new Square();
var area = sq.getArea();
// JS:
// var area = Square.getArea(sq);
var anySq: any = sq;
var anyArea = anySq.getArea(); // Still no Intellisense?
// JS:
// var anySq = sq;
// var anyArea = anySq.getArea();
// works as expected, because it is on the prototype
sq.depth = 10;
var volume = sq.getVolume();
// JS:
// sq.depth = 10;
// var volume = sq.getVolume();
Basically, what I am trying to do is sort of follow what the dynamic
keyword would do in C#, but also permit the explicit extension of an existing prototype like JavaScript.
I am still not too sure what the best is for the class declaration:
extension class MyExtensions<Square> { }
class MyExtensions extensionof Square { }
extension class MyExtensions extension Square { }
might be best to avoid the instantiation of MyExtensions
... C# required that the class and method be static, and the method have the this
. But, JavaScript does allow the extension if instance members, so static can't be a requirement. Maybe we could say that if the method was explicitly marked as extension
, then the first this
parameter can be removed and you can use the this
keyword in the method body itself. This way the extension fields and existing fields can work the same?
Case 1 would be the best thing since sliced bread because it'll work with interfaces, generics, builtins and third party types extremely well. It's an extremely modular feature, and will work automatically with future extensions of the type system.
Case 2 only works for extending classes, can't work on interfaces, can't work on builtins without polluting them, and can't work only on select generic type parametrizations.
For example - in Case 1 - when imported, your getArea method will be available on an HTMLCanvas element, because it has width and height properties. How awesome is that?
On Saturday, 11 June 2016, Matthew Leibowitz notifications@github.com wrote:
Just wanted to give my humble suggestion, pardon my two-week TypeScript skills.
I have been reading this discussion and maybe we are trying to do something primarily designed for type-based systems. JavaScript is much less typey, so work with that (this syntax is just one way):
Given this class:
class Square { width: number; height: number; }
Case 1: Does not touch the prototype:
extension class SimpleSquareExtensions { depth: number = 0; static scale: number = 0;
// must be static static function getArea(this square: Square) { // Can't access `depth` at all, not static // Can't do this: `square.scale` - not on Square // Can do this: `SimpleSquareExtensions.scale` return square.width * square.height; }
}
// JS: SimpleSquareExtensions.scale = 0; SimpleSquareExtensions.prototype.depth = 0; SimpleSquareExtensions.getArea = function (square) { return square.width * square.height; }
This is a very basic type that does not touch the Square prototype, so:
var sq: Square = new Square();
var area = sq.getArea(); // JS: // var area = SimpleSquareExtensions.getArea(sq);
var anySq: any = sq; var anyArea = anySq.getArea(); // No Intellisense for getArea() // JS: // var anyArea = anySq.getArea(); // Runtime error - expected
This works like C# dynamic object, I can't apply a Square extension method to an object that might not be a square.
Also, we could, or not, permit other non-extension members on the class, such as the depth and scale properties. (could be methods too)
Case 2: Adds to the prototype:
extension class MoreSquareExtensions { extension static scale: number = 0; extension depth: number = 0;
extension static function getArea(this square: Square) { // Can't do this: `square.depth` - static method // Can't do this: `Square.depth` - not static field // Can't do this: `MoreSquareExtensions.depth` - not on MoreSquareExtensions // Can't do this: `square.scale` - static field // Can't do this: `MoreSquareExtensions.scale` - not on MoreSquareExtensions // Can do this: `Square.scale` return square.width * square.height; } // no need for static extension function getVolume(this square: Square) { // Can't do this: `Square.depth` - not static field // Can't do this: `MoreSquareExtensions.depth` - not on MoreSquareExtensions // Can't do this: `square.scale` - static field // Can't do this: `MoreSquareExtensions.scale` - not on MoreSquareExtensions // Can do this: `square.depth` // Can do this: `Square.scale` return square.width * square.height * square.depth; }
}
// JS: Square.scale = 0; Square.prototype.depth = 0; Square.getArea = function (square) { return square.width * square.height; } Square.prototype.getVolume = function () { return this.width * this.height * this.depth; }
Due to the extension keyword, the developer is instructing the compiler to modify the type. The dev is responsible for resolving conflicts:
var sq: Square = new Square();
var area = sq.getArea(); // JS: // var area = Square.getArea(sq);
var anySq: any = sq; var anyArea = anySq.getArea(); // Still no Intellisense? // JS: // var anySq = sq; // var anyArea = anySq.getArea(); // works as expected, because it is on the prototype
sq.depth = 10; var volume = sq.getVolume(); // JS: // sq.depth = 10; // var volume = sq.getVolume();
— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/Microsoft/TypeScript/issues/9#issuecomment-225385496, or mute the thread https://github.com/notifications/unsubscribe/AASm69pd5PdoRiuIuBf1_-E7L560SIVDks5qKwScgaJpZM4CNZ1k .
@mattleibow seems like you're proposing partial classes, see #563
can we extension interface? interface can also have some implemented functions, just like Swift:
interface Rect {
x: number
y: number
}
extension Rect {
area() => this.x * this.y
}
Swift version:
protocol Rect {
var x: Float {get}
var y: Float {get}
}
extension Rect {
func area() -> Float {
return self.x * self.y
}
}
can we extension interface?
It can be dangerous: https://github.com/Microsoft/TypeScript/issues/9#issuecomment-52329918, https://github.com/Microsoft/TypeScript/issues/9#issuecomment-52347966
@SaschaNaz I don't think so. Swift already deal with it. It just add a prototype function to every class which implements it.
There is many detail works done by Swift team in Swift to solve the worry about the different package issue.
@SaschaNaz I think interface extension is no more dangerous than class extension.
The way to solve the conflicting problem is using developer‘s import to decides which implementation should use. And also when package author extends some global interface or properties, they always use perfix to identify the extension. For example, RxSwift package extends UIView class with rx_dellocated. When developer import "RxSwift", the UIView will have rx_deallocated property. If developer don't import "RxSwift", there is no rx_deallocated with UIView.
Given the constraints imposed by emit (see https://github.com/Microsoft/TypeScript/issues/9#issuecomment-74302592), there are realistically only two paths forward for object-first non-property method invocations:
I think we're reasonably satisfied (minus bikeshedding) on partial classes, and these can already be emulated today with a not-distasteful syntax (elucidated here: https://github.com/Microsoft/TypeScript/issues/563#issuecomment-218016147). I'd encourage anyone to weigh in meaningfully on extension
vs partial
vs other keywords, keeping in mind that we didn't just say "yea partial
sounds good" and be done with it.
The ES20xx 'bind' operator ('xx' because who knows what version) is still in semantic flux and we need that proposal to either die cleanly (to let us lay clear claim to that syntactic space), or proceed with clear semantics, before we can act on it. It's an area where TypeScript can't safely act unilaterally. There's good discussion going on at https://github.com/tc39/proposal-bind-operator/issues/24 and its parent repo that outlines next steps for the proposal and it'd be great for people to perform citizen advocacy at ESDiscuss or other forums to help this move forward.
I'm not sure if this has been suggested yet (this is a long thread), but what about compiler warning or compiler errors with explicit override (like eslint "ignore this" comments) for call site replacement?
That is in the situation implied in this comment , instead of silently failing in the second example an error is emitted akin to "extension method called on unknown type" and the following code can be used to remove the error:
process(((n) => {
n.getArea(); //@compiler:no extension
}));
I haven't seen this type of explicit compiler hints in typescript, and perhaps it's intentional, but I think that call site replacement is a very powerful tool that a majority of developers would love to use, and the situation described in the comment will almost never happen. I don't think Typescript should fall into undefined behaviour at runtime, but I think that an error and an explicit optout will prevent anyone from unwittingly making this mistake.
Allow a declarative way of adding members to an existing type's prototype
Example: