Skip to content
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

Declare static non-method members within class' prototype #3743

Closed
GoToLoop opened this issue Jul 5, 2015 · 23 comments
Closed

Declare static non-method members within class' prototype #3743

GoToLoop opened this issue Jul 5, 2015 · 23 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@GoToLoop
Copy link

GoToLoop commented Jul 5, 2015

This TS code for class declaration :

class SomeClass {
  static CONSTANT = 10; // Wanna insert it as a prototype member!
  showStatic() { alert(SomeClass.CONSTANT); return this; } // OK...
  showPrototype() { alert(this.CONSTANT); return this; }   // Error!
}

new SomeClass().showStatic().showPrototype(); // Displays 10, then undefined.

Transpiles into the following JS code:

var SomeClass = (function () {
  function SomeClass() {
  }

  SomeClass.prototype.showStatic = function () { alert(SomeClass.CONSTANT); return this; }; // OK...
  SomeClass.prototype.showPrototype = function () { alert(this.CONSTANT); return this; }; // Error!
  SomeClass.CONSTANT = 10; // Wanna insert it as a prototype member!
  return SomeClass;
})();

new SomeClass().showStatic().showPrototype(); // Displays 10 then undefined.

Playground link:
http://www.typescriptlang.org/Playground#src=class%20SomeClass%20%7B%0D%0A%20%20static%20CONSTANT%20%3D%2010%3B%20%2F%2F%20Wanna%20insert%20it%20as%20a%20prototype%20member!%0D%0A%20%20showStatic()%20%7B%20alert(SomeClass.CONSTANT)%3B%20return%20this%3B%20%7D%20%2F%2F%20OK...%0D%0A%20%20showPrototype()%20%7B%20alert(this.CONSTANT)%3B%20return%20this%3B%20%7D%20%20%20%2F%2F%20Error!%0D%0A%7D%0D%0A%0D%0Anew%20SomeClass().showStatic().showPrototype()%3B%20%2F%2F%20displays%2010%20then%20undefined.

Does TS have any syntax to insert static non-method properties into their class' prototype?
So they can be accessed w/ this and be inheritable just like methods are?
If not, do you plan on add this useful feature or have some workaround?

@LordJZ
Copy link

LordJZ commented Jul 5, 2015

class SomeClass {
  CONSTANT: number;
  showPrototype() { alert(this.CONSTANT); return this; }
}
SomeClass.prototype.CONSTANT = 10;

new SomeClass().showPrototype(); // Displays 10

@GoToLoop
Copy link
Author

GoToLoop commented Jul 5, 2015

Thx for the attempt but you've removed static from CONSTANT.
It means if we instantiate SomeClass 1000x, we're gonna get 1001 CONSTANT properties rather than 1 only!
My ultimate goal is to create "d.ts" files for libraries which got non-method members in their prototypes.

@LordJZ
Copy link

LordJZ commented Jul 6, 2015

Nope, instantiate SomeClass 1000 times and you only get 1 property. Describe a non-method prototype member just as you would describe a method prototype member, the way I've shown.

@RyanCavanaugh RyanCavanaugh added the Question An issue which isn't directly actionable in code label Jul 6, 2015
@RyanCavanaugh
Copy link
Member

@LordJZ 's suggestion is correct.

Putting non-method members on the prototype is usually a bad idea; we likely won't add special syntax for this unless there's some compelling reason to do so. The approach of setting it manually immediately after the class declaration is a good solution for people who really know what they're doing.

@GoToLoop
Copy link
Author

GoToLoop commented Jul 6, 2015

  • Indeed you're right! B/c TS won't emit JS code for non-initialized properties, CONSTANT doesn't end up becoming an instance variable of SomeClass.
  • So as long as any assignment happens outside the class block, it's gonna go straight to SomeClass.prototype.
  • Only problem is that for any1 checking out CONSTANT is gonna think it is an instance variable rather than its true nature as a class variable stored in SomeClass.prototype.
  • A pity you said you're not interested about adding some proper idiom for non-methods stored in prototype.
  • it coulda been called (non-method), (proto-property) or simply (proto). Whatever! ;-)
  • Dunno whether it's compelling enough... But as I've mentioned, I'm doing a ".d.ts" file for an existing JS framework.
  • Of course it'd be gr8 if we could precisely describe non-methods stored in prototype for interfaces as well.
  • Since you've already closed this issue thread, this proposal won't be appreciated for the rest of the team I'm afraid. :-(
  • For comparison reasons, here's some CoffeeScript sample demoing what I wished for TS too:
class SomeClass

  SomeClass.TS_CONSTANT = 10
  CS_CONSTANT: 20

  showStatic:    -> alert('TS_CONSTANT: ' + SomeClass.TS_CONSTANT); @
  showPrototype: -> alert('CS_CONSTANT: ' + @CS_CONSTANT); @

new SomeClass().showStatic().showPrototype()

It emits the following JS:

var SomeClass;

SomeClass = (function() {
  function SomeClass() {}

  SomeClass.TS_CONSTANT = 10;

  SomeClass.prototype.CS_CONSTANT = 20;

  SomeClass.prototype.showStatic = function() {
    alert('TS_CONSTANT: ' + SomeClass.TS_CONSTANT);
    return this;
  };

  SomeClass.prototype.showPrototype = function() {
    alert('CS_CONSTANT: ' + this.CS_CONSTANT);
    return this;
  };

  return SomeClass;

})();

new SomeClass().showStatic().showPrototype();

Online link for it:
http://CoffeeScript.org/#try:class%20SomeClass%0A%0A%20%20SomeClass.TS_CONSTANT%20%3D%2010%0A%20%20CS_CONSTANT%3A%2020%0A%0A%20%20showStatic%3A%20%20%20%20-%3E%20alert('TS_CONSTANT%3A%20'%20%2B%20SomeClass.TS_CONSTANT)%3B%20%40%0A%20%20showPrototype%3A%20-%3E%20alert('CS_CONSTANT%3A%20'%20%2B%20%40CS_CONSTANT)%3B%20%40%0A%0Anew%20SomeClass().showStatic().showPrototype()

@RyanCavanaugh
Copy link
Member

I don't understand why you'd care, from a type system perspective, whether a property is initially defined in the prototype or not. It seems more like the kind of thing you'd put in the documentation.

What kinds of things would be errors for a prototype-defined property that wouldn't be an error for a non-prototype property (or vice versa)?

@GoToLoop
Copy link
Author

GoToLoop commented Jul 6, 2015

  • Most obvious error is that TS static style properties can't be accessed via this!
  • While class variables stored in prototype can be accessed by 3 diff. ways:
    1. this.CONSTANT;
    2. this.__proto__.CONSTANT; // (btW __proto__ isn't automatically recognized by TS!)
    3. SomeClass.prototype.CONSTANT;
  • So we can access it via an instance of the class:
    someMethod(instance: SomeClass) { return instance.CONSTANT == 10; }
  • While we can't use the instance parameter for TS static style:
    someMethod(instance: SomeClass) { return SomeClass.CONSTANT == 10; }
  • And also a big bonus: Anything inside the prototype is inheritable, just like any methods & static fields are in Java!
  • TS static style properties don't really belong to the class lineage. It's merely an appendage!

@about-code
Copy link
Contributor

Although this issue has unfortunately already been closed I'd like to point out on the discussion at [1] for reference. In this discussion "DerCapac" proposed to introduce a special keyword like shared or mutual for these kind of properties. Personally, I would love to see a keyword, too.

My main concerns why I'd vote for a keyword are readability and verbosity: IMHO the workaround suggested by @LordJZ or @ahejlsberg [1] to write MyClass.prototype.foo = "bar"; at the end of a potentially lengthy class definition is not very satisfying, exactly because such statements have non-trivial consequences, as has been pointed out by the typescript team as well. So when it comes to readability I'd prefer to have a clear concept, such as a keyword, which helps at least in discussing the consequences of using that concept.

In terms of verbosity I found that it is not enough to write MyClass.prototype.foo = "bar" but one has also to declare the property on the class itself to type it. Yet one must be aware not to accidentally initialize the property because it would create an own property which "hides" the prototype property initialized outside the class. This way what we write at the top and inside a class declaration may be driven by a statement which was written at the bottom and outside of it. Is this really favourable?

Example:

class MyClass {
     foo:string; // don't initialize if you don't want to hide MyClass.prototype.foo
}
MyClass.prototype.foo = "mySharedValue".

Its apparent that something like

class MyClass {
     shared foo:string = "mySharedValue";
}

would be much more concise.

@RyanCavanaugh I would also be interested to know why we should care about the issue from a type system perspective, but personally I would like to see more compelling arguments where this JS concept is fundamentally conflicting with a type system. I don't think it does because the TS compiler can already handle it properly when using the verbose syntax.

Put differently, why are static properties so much more interesting from a type system perspective that they have gotten their own keyword? Instead we similarily could write MyClass.myStaticProperty = "myStaticValue" outside of a class definition. I guess it is just there, because people coming from a statically typed language are familiar with it and are likely to expect it. Well, I ask you to reevaluate, if people coming from JS might could expect something like shared.

I think the actual point is more about language design and why there is syntactic sugar for certain concepts whereas it is refused for others. Refusing a keyword because there are consequences which might be unfamiliar to people coming from statically typed languages is reasonable but IMHO also very debatable. I'd like to argue that the proposed workaround makes it harder for these people to explore the benefits and pitfalls of this particular JS concept whereas a keyword would provide a clear name to the concept and help writing less verbose, more readable code.

[1] https://typescript.codeplex.com/discussions/444777

@GoToLoop
Copy link
Author

GoToLoop commented Dec 8, 2015

I feel this issue was unfairly & abruptly "replied + closed" w/o much thought.
And hidden away before it had more eyes to examine in the proposal.

@devpunk, excellent 2-year-old discussion dig btW: https://typescript.CodePlex.com/discussions/444777
Very interesting they had already thought about the need for some extra keyword for it.

I was motivated to open this issue here b/c I was making a ".d.ts" file for some JS library.
Reality is that many JS libs put in non-method properties in the prototype too.
And TS should recognize that they're there. Also by hovering over them in the IDE.

Until then, I've finally found out a much better actual workaround.
1 that doesn't demand declaring them as regular instance variables.

However, it demands that we have 2 extra interfaces.
And 1 of them inside a namespace w/ the same class' name.

The interface outside the namespace has the same name as the class and extends the internal interface.
While namespace's internal interface is surprisingly called prototype.

However, we can't initialize them aFaIK b/c they're described inside an interface. =(
At least this solution works for class inheritance as well! Check it out:

class SomeClass {
  __proto__: this;
  showProtoConst() { alert(this.CONSTANT); return this; }
}

interface SomeClass extends SomeClass.prototype {}

namespace SomeClass {
  export interface prototype {
    CONSTANT: number;
  }
}

class SubClass extends SomeClass {}

SomeClass.prototype.CONSTANT = 10;
const val = new SomeClass().showProtoConst();
console.info(val.CONSTANT);

SomeClass.prototype.CONSTANT = 20;
console.info(val.CONSTANT);

const sub = new SubClass().showProtoConst();
console.info(sub.CONSTANT);

SomeClass.prototype.CONSTANT = 30;
console.info(sub.CONSTANT);

SubClass.prototype.CONSTANT = 40;
console.info(sub.CONSTANT);

console.info(val.CONSTANT); // still 30 though.

http://www.TypeScriptLang.org/Playground#src=class%20SomeClass%20%7B%0A%20%20__proto__%3A%20this%3B%0A%20%20showProtoConst()%20%7B%20alert(this.CONSTANT)%3B%20return%20this%3B%20%7D%0A%7D%0A%0Ainterface%20SomeClass%20extends%20SomeClass.prototype%20%7B%7D%0A%0Anamespace%20SomeClass%20%7B%0A%20%20export%20interface%20prototype%20%7B%0A%20%20%20%20CONSTANT%3A%20number%3B%0A%20%20%7D%0A%7D%0A%0Aclass%20SubClass%20extends%20SomeClass%20%7B%7D%0A%0ASomeClass.prototype.CONSTANT%20%3D%2010%3B%0Aconst%20val%20%3D%20new%20SomeClass().showProtoConst()%3B%0Aconsole.info(val.CONSTANT)%3B%0A%0ASomeClass.prototype.CONSTANT%20%3D%2020%3B%0Aconsole.info(val.CONSTANT)%3B%0A%0Aconst%20sub%20%3D%20new%20SubClass().showProtoConst()%3B%0Aconsole.info(sub.CONSTANT)%3B%0A%0ASomeClass.prototype.CONSTANT%20%3D%2030%3B%0Aconsole.info(sub.CONSTANT)%3B%0A%0ASubClass.prototype.CONSTANT%20%3D%2040%3B%0Aconsole.info(sub.CONSTANT)%3B%0A%0Aconsole.info(val.CONSTANT)%3B%20%2F%2F%20still%2030.

@about-code
Copy link
Contributor

@GoToLoop I don't think they wanted to be unfair, but simply got a lot more issues to deal with. I share your opinion that the topic deserved more discussion. The underlying problem of setting prototype properties might not even be specific to TypeScript but may require a solution in ES6 classes as well.

As far as TypeScript is concerned the answers I could find so far may be best summed up as we don't want to give fame to a JavaScript concept which we consider bad practice. I think its a valid argument but I tried to show why it might eventually turn out to contradict the good intentions they actually have in mind.

I am not sure if I fully understand why you didn't declare a regular instance variable thats initialized outside the class {}-block. Without explicit initialization it would remain undefined, yet being available to code completion.

(by the way: funny avatar :)

@GoToLoop
Copy link
Author

GoToLoop commented Dec 9, 2015

... why you didn't declare a regular instance variable that's initialized outside the class {}-block.

Sorry I don't get what you mean? The only instance variable from class SomeClass is __proto__
And it's irrelevant for the prototype hack workaround I did.

Variables val & sub refer respectively to instances of class SomeClass & SubClass.
And if we hover over those 2, we'll see their types match.
Also hovering over val.CONSTANT will show it dwells in SomeClass.prototype, as my hack intended so.

@GoToLoop
Copy link
Author

GoToLoop commented Dec 9, 2015

... we don't want to give fame to a JavaScript concept which we consider bad practice.

  • As I've pointed out, main reason for the feature is for ".d.ts" files for already existing JS libraries.
  • 2nd reason is that those non-method properties inside prototype can be inherited and accessed by this as well.
  • TS static properties aren't either inheritable nor accessible by this.
  • As a Java programmer, I'm used to access static fields & methods by this too.
  • Therefore, it's not some so-called "bad" concept; but used & expected in JS, Java & C#. Perhaps in more languages too!

@about-code
Copy link
Contributor

... why you didn't declare a regular instance variable that's initialized outside the class {}-block.

Sorry, I used a bit awkward language here. I thought about using @LordJZ's proposal but forgot that you weren't satisfied with it because

Only problem is that for any1 checking out CONSTANT is gonna think it is an instance variable rather than its true nature as a class variable stored in SomeClass.prototype.

Note "true nature as class variable" in the sense of Java is not possible to achieve in JavaScript. The way it is currently solved in TS is what comes most closely to class variables (at the cost of inheritance and this accessibility). This is why another keyword in addition to static was proposed, specifically for prototype properties whose behavior may be called "semi-static".

I'm used to access static fields & methods by this too.
Therefore, it's not some so-called "bad" concept; but used & expected in JS, Java & C#.

With a Java background you may not used to the fact that when assigning a value to a static attribute using this will in fact create an instance property on the instance which is referenced by this. That's, however what happens with prototype properties in TS/JS:

Example:

class MyClass() {
    mySemiStaticVar:string;
    read() {
         console.log(this.mySemiStaticVar);
    }
    write(value:string) {
        this.mySemiStaticVar = value;
    }
}
MyClass.prototype.mySemiStaticVar = "Hello"; // the non-method prototype member

// Create instances
var inst1 = new MyClass();
var inst2 = new MyClass();

console.log(inst1.hasOwnProperty("mySemiStaticVar")); // false 
inst1.read(); // Hello     (looked up along prototype chain)

console.log(inst2.hasOwnProperty("mySemiStaticVar")); // false 
inst2.read(); // Hello    (looked up along prototype chain)

inst2.write("World");   // inst2 now gets its own mySemiStaticVar
console.log(inst2.hasOwnProperty("mySemiStaticVar")); // true 
console.log(inst1.hasOwnProperty("mySemiStaticVar")); // false 

inst1.read(); // Hello     (looked up along prototype chain)
inst2.read(); // World!  (looked up from own property)
console.log(inst2.constructor.prototype.mySemiStaticVar)  //  What do you expect?

If you write a variable with this.myVar = "myValue" where myVar only exists on the prototype of this.constructor prior to the assignment, then a new OwnProperty is created as part of the assignment on whatever this refers to. Once created, the next time you read/write the property using this, you will read/write an instance-specific incarnation rather than the instance-independent static property (as e.g. in Java). Reason is that JS creates any property used on the left hand side of an assignment expression dynamically if it doesn't exist.

This is where your example becomes a bit hard to talk about because you unfortunately call your prototype member variable CONSTANT which suggests not to talk about the write case. However the write case matters to prototype properties and static variables per sé are writable, too.

@GoToLoop
Copy link
Author

Note "true nature as class variable" in the sense of Java is not possible to achieve in JavaScript.

The OOP notion of class variable is 1 solo static field / property which is shared among instances of its class.
This is completely achievable in JS by putting them inside the class / constructor function's prototype.

The way it is currently solved in TS is what comes most closely to class variables...

As I've mentioned before, the way static is used by TS is foreign on how Java achieves its static-ness.
Since classes don't have properties in Java, everything is stored in 1 prototype place only.

When we create an object in Java, only non-static fields are actually cloned and placed in it.
Everything else: constructors, static fields, static & non-static methods stay in the class.
In JS terms, that class place is called the prototype. That is, the place which is static & unaffected by new.

Every Java object got some extra data too, including the reference for the original class.
In JS parlance, that reference is the "hidden" __proto__. ;-)

When we invoke a Java method, Java doesn't even bother looking for it inside the object.
It goes straight to the object's class source via its __proto__-like embedded data.
And at the same time, the object passes its reference to the method called so it internally becomes its this.
Very much like JS passes the object's reference to become this when invoking a function!

Of course other JS class places apart from prototype are also static.
But can't be reached by an instance reference b/c usually an object's __proto__ points to the constructor's prototype property and not to some other arbitrary property of it.

@GoToLoop
Copy link
Author

... because you unfortunately call your prototype member variable CONSTANT...

I was adapting from my 1st example which already had a class variable called CONSTANT.
Of course in real code I would assign a value to it once and never change it during the app's execution. :-P

With a Java background you may not used to the fact that when assigning a value to a static attribute using this will in fact create an instance property on the instance which is referenced by this.

I was very aware of that. As much as I only used the assignment operator accompanied by prototype:
SomeClass.prototype.CONSTANT = 10;

Assignments over this creates hasOwnProperty() for anything, including methods too!
Therefore, for any new own member in that object, its corresponding "cousin" in prototype is lost.
B/c once a property is found, it won't follow __proto__ anymore. Unless we delete it. HEHE

In short, we can only read from a prototype member and never use the assignment operator over this.
That includes composite assignment operators plus unary increment & decrement operators too!

P.S.: Your hasOwnProperty() example emit errors in TS b/c you haven't declared mySemiStaticVar as an instance variable as well! Either apply my hack or @LordJZ '. O_o

@GoToLoop
Copy link
Author

Go to https://www.CompileJava.net/ in order to paste, compile & run the Java example below.
Java prints out: 10, 30, 25, 25.
TS prints out: 10, 30, 25, 30.

Java Example:

public class SomeClass {
  int instance_var = -99;      // it's cloned and goes to "this".
  //static ts_static_var = -1; // no exact correspondence in Java!
  static int static_var = 10;  // stays in the class' "prototype".

  public static void main(String[] args) {
    final SomeClass some = new SomeClass();

    System.out.println(SomeClass.static_var); // "prototype" direct access.
    SomeClass.static_var *= 3;                // "prototype" direct assignment.
    System.out.println(some.static_var);      // "prototype" access via "this".

    // We can't direct use any assignment operators in JS via "this"
    // For it creates an "own" property on-the-fly
    // Rather than changing the 1 inside the prototype
    // But since Java can't create fields on-the-fly
    // It infers we wanna change the static "prototype" field:

    some.static_var -= 5; // changes "prototype" in Java but "this" in JS!
    System.out.println(some.static_var); // prints out 25
    System.out.println(SomeClass.static_var); // 25 in Java but still 30 in JS
  }
}

TS Example:

class SomeClass {
  instance_var = -99;        // it's cloned and goes to "this".
  static ts_static_var = -1; // no exact correspondence in Java!
}

interface SomeClass extends SomeClass.prototype {}

namespace SomeClass {
  export interface prototype {
    static_var: number;  // stays in the class' "prototype".
  }
}

SomeClass.prototype.static_var = 10;

const some = new SomeClass;

console.info(SomeClass.prototype.static_var); // "prototype" direct access.
SomeClass.prototype.static_var *= 3;          // "prototype" direct assignment.
console.info(some.static_var);                // "prototype" access via "this".

// We can't direct use any assignment operators in JS via "this"
// For it creates an "own" property on-the-fly
// Rather than changing the 1 inside the prototype
// But since Java can't create fields on-the-fly
// It infers we wanna change the static "prototype" field:

some.static_var -= 5; // changes "prototype" in Java but "this" in JS!
console.info(some.static_var); // prints out 25
console.info(SomeClass.prototype.static_var); // 25 in Java but still 30 in JS

http://www.TypeScriptLang.org/Playground#src=class%20SomeClass%20%7B%0A%20%20non_static_var%20%3D%20-99%3B%20%20%20%20%20%20%2F%2F%20it's%20cloned%20and%20goes%20to%20%22this%22.%0A%20%20static%20ts_static_var%20%3D%20-1%3B%20%2F%2F%20no%20correspondence%20in%20Java!%0A%7D%0A%0Ainterface%20SomeClass%20extends%20SomeClass.prototype%20%7B%7D%0A%0Anamespace%20SomeClass%20%7B%0A%20%20export%20interface%20prototype%20%7B%0A%20%20%20%20static_var%3A%20number%3B%20%20%2F%2F%20stays%20in%20the%20class'%20%22prototype%22.%0A%20%20%7D%0A%7D%0A%0ASomeClass.prototype.static_var%20%3D%2010%3B%0A%0Aconst%20some%20%3D%20new%20SomeClass%3B%0A%0Aconsole.info(SomeClass.prototype.static_var)%3B%20%2F%2F%20%22prototype%22%20direct%20access.%0ASomeClass.prototype.static_var%20*%3D%203%3B%20%2F%2F%20%22prototype%22%20direct%20assignment.%0Aconsole.info(some.static_var)%3B%20%2F%2F%20%22prototype%22%20access%20via%20%22this%22.%0A%0A%2F%2F%20We%20can't%20direct%20use%20any%20assignment%20operators%20in%20JS%20via%20%22this%22%0A%2F%2F%20For%20it%20creates%20an%20%22own%22%20property%20on-the-fly%0A%2F%2F%20Rather%20than%20changing%20the%201%20inside%20the%20prototype%0A%2F%2F%20But%20since%20Java%20can't%20create%20fields%20on-the-fly%0A%2F%2F%20It%20infers%20we%20wanna%20change%20the%20static%20%22prototype%22%20field%3A%0A%0Asome.static_var%20-%3D%205%3B%20%2F%2F%20changes%20%22prototype%22%20in%20Java%20but%20%22this%22%20in%20JS!%0Aconsole.info(some.static_var)%3B%20%2F%2F%20prints%20out%2025%0Aconsole.info(SomeClass.prototype.static_var)%3B%20%2F%2F%2025%20in%20Java%20but%20still%2030%20in%20JS

@about-code
Copy link
Contributor

P.S.: Your hasOwnProperty() example emit errors in TS b/c you haven't declared mySemiStaticVar as an instance variable as well! Either apply my hack or @LordJZ '. O_o

You're right. I have updated my example which I wrote in plain JS in a Chrome Console and tried to properly adapt it to TS syntax. Well, you see why I support a more convenient syntax. ;-).

The OOP notion of class variable is 1 solo static field / property which is shared among instances of its class.

If that's your premise I'd agree, that's possible to achieve. But its another question if it's wise to employ the prototype to achieve it.

In short, we can only read from a prototype member and never use the assignment operator over this.
[...]
I'm used to access static fields & methods by this too.
Therefore, it's not some so-called "bad" concept; but used & expected in JS, Java & C#.

From access I entailed you mean read and write access over this. Of course, the bad concept may not be the access via this per sé * in languages where it works for read and write. But the bad idea may be to implement static in JS via prototype properties in order to gain inheritance and access via this, given that an assignment over this doesn't mean to manipulate the shared field.

Said this, I still think it would be nice to have better syntax for members on prototypes, not for resembling a concept from other languages but for the reasons outlined earlier.

*AFAIK you even get a warning in some IDEs like e.g. Eclipse if you try do the same in Java.

@GoToLoop
Copy link
Author

... given that an assignment over this doesn't mean to manipulate the shared field.

I'm not asking for TS to have 100% safe-guards against "shared" re-assignments.
Java/C# don't have such issue b/c they don't create fields on-the-fly like JS/Lua.

So it's crystal clear for the former when we attempt to re-assign some static field via this, we haven't meant for some nonexistent instance field.

But that's ambiguous for JS! And a brand new instance property is born, canceling the "shared" 1 for that particular instance, when that happens.

Nonetheless, I believe it wouldn't be that hard to protect against creating instance variables outta static 1s inside classes. Since they would be properly identified as shared/mutual props. after all. :-D

*AFAIK you even get a warning in some IDEs like e.g. Eclipse if you try do the same in Java.

Warnings aren't errors and I did the Java example from Notepad2-mod! ;-)

@avonwyss
Copy link

avonwyss commented Jan 2, 2016

I'm just starting to use TypeScript to convert from a large CoffeeScript code base to gain the advantages of the type system.

That being said, just as demonstrated by GoToLoop earlier, this construct is easily available in CoffeeScript and thus also used frequently. In the codebase I'm converting it is used in most classes for defining default values, and for storing metadata that belongs to the class of the instance (the "static" scenario, without having to access the constructor property to get them).

Taking the example from the TS spec https://github.com/Microsoft/TypeScript/blob/master/doc/spec.md#841-member-variable-declarations with less fields but with an added instance method we have the following:

class Employee {  
    public name: string;  
    public address: string;  
    public retired = false;
    public test() {}
}

Which is equivalent to:

class Employee {  
    public name: string;  
    public address: string;  
    public retired: boolean;  
    constructor() {  
        this.retired = false;  
    }
    public test() {} 
}

But looking at the source, the behavior looks inconsistent; the member fields are handled differently from the member methods. What I expected when I was converting the CoffeeScript classes was that they would behave in a consistent way, e.g. the field would only be initialized in the constructor when that was actually coded that way, and otherwise on the prototype like that (desired JS output):

var Employee = (function () {
    function Employee() {}
    Employee.prototype.retired = false;
    Employee.prototype.test = function () { };
    return Employee;
})();

This also has runtime performance implications; having the initializations on the prototype makes it so that creating new instances requires less code to run (especially in classes with several fields what should have an initial value) and it may also reduce their memory footprint since no memory needs to be allocted per instance for the properties on the protoype chain.

Now I understand that given the current state this is not going to be changed, but there should at least be support for it without the explicit assignment after creation of the object. A keyword would be great, maybe like this:

class Employee {  
    public name: string;  
    public address: string;  
    public retired default false;
    public test() {}
}

I'd propose the "default" keyword followed by an expression, since this even allows to keep both in the same line (as in x default 0 = 1). The reason why I think that "default" is a good fit is because it mirrors the fact quite well: when a property is not explicitly present (e.g. hasOwnProperty is false), that's the value you're going to get, also when delete is used on a property. In addition to that, the position of the keyword would avoid a parsing ambiguity with property names because as far as I can tell the only valid tokens at this position are = or ;.

I'm not sure why this is supposed to be a bad practice in general, at least not for types which are immutable (primitives, strings). The compiler could still emit a warning when one uses this syntax for arrays or other mutable objects.

@about-code
Copy link
Contributor

@avonwyss
By initializing value and function properties differently, TS follows established (JS) best practices. In fact the simplest and common pattern to class-like structures in plain JavaScript is something like (see also [1] and [2])

var MyClass = function() {
  this.foo = "foo";
  this.bar = "bar;
}
MyClass.prototype.myMethod() {
   // do something fancy here...
}

Transpilation of TS members vs. methods might seem inconsistent when thinking of methods as just being properties, too. But it is quite consistent with OOP if you think of state and behavior instead, where members reflect state and function properties are behavior. Most commonly you are likely to have instances of a class to have their own state (-> OwnProperties) but share common behavior (-> Proto.-Prop.) so it makes sense to treat members and methods differently.

With static TS has a keyword to implement shared state, too, when necessary, but it does set it on the constructor function of instances while it is also very common in JS to put shared state on the prototype. Yet the latter may have side effects not easily anticipated by people from statically typed languages. Therefore I agree with TS maintainers, that it would likely have been confusing for many people to implement static this way. Yet, I'd be happy to have a special keyword and TS concept to also express shared state via prototype properties.

I'd propose the "default" keyword followed by an expression, since this even allows to keep both in the same line

class Employee {  
    public name: string;  
    public address: string;  
    public retired default false;
    public test() {}
}

I think in terms of (syntactic) consistency all modifiers should be before the actual property name. Further I don't see why you would like to omit the assignment operator. So for the sake of syntactic consistency I think

class Employee {  
    public name: string;  
    public address: string;  
    public default retired: boolean = false;
    public test() {}
}

would be better, yet I think default is not optimal. For example, as you pointed out, member initializations without this special keyword translate to

constructor() {  
        this.retired = false;  
    }

which can be read as: any new instance of Employee will have retired to be false by default. So having a special default keyword which does translate to something else is likely to confuse people.

[1] Douglas Crockford: JavaScript - The good parts; O'Reilly. 2008. Chapter 4 (Augmenting Types)
[2] Addy Osmany: JavaScript Patterns; O'Reilly. Online: https://addyosmani.com/resources/essentialjsdesignpatterns/book/#constructorpatternjavascript

@avonwyss
Copy link

avonwyss commented Jan 4, 2016

@devpunk , thanks for your thoughts. Regarding the inconsistency, one of the patterns which is also used in our code base is to have "abstract" methods on non-abstract classes, e.g. the code performs a falsyness check before invoking them. Translated to TS, this would be something like this:

class a {
    x: () => boolean;
    y(): boolean { return this.x ? this.x() : true; }
}

class b extends a {
    x(): boolean { return false; } // compiler error
}

Since abstract methods can only appear in abstract classes, I cannot use the abstract keyword, yet since I cannot define them properly on the protoype I also cannot use a construct like the code above. Frankly, even though I'm coming from a statically typed language background, the way this has been implemented is a leaky abstraction and it feels like a hack.

I think in terms of (syntactic) consistency all modifiers should be before the actual property name. Further I don't see why you would like to omit the assignment operator.

Because my suggestion is in fact to use an alternative assignment operator to set the prototype property value, and not a modifier per se. In the end, when reading values, it does not make a difference, and it is already so that fields defined in a class are permitted on its prototype. That being said, there may even be sitations where you actually want both assignments.

@about-code
Copy link
Contributor

I think the pattern you describe should be discussed separately because this issue is about "non-method prototype members". The compile error you get is about redefining a member property as a method:

"Class 'a' defines instance member property 'x', but extended class 'b' defines it as instance member function"

Even though: you might be able to apply a similar workaround to get around the compile error (disclaimer: the workaround only works if you don't need access to super within of x()).

class b extends a {}
b.prototype.x = function() { return false; }; // no compiler error

@phfsantos
Copy link

@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

6 participants