Suggestion: 'protected' modifier
RyanCavanaugh opened this issue · 27 comments
General proposal
protected
modifier acts the same as private
in terms of code generation and assignability, except that it is possible to access a protected
member in any subclass of a class which declared the member. Basically, you can access protected members in the same situations where it would have been an error for you to redeclare a private
member with the same name as a base class's member.
Examples
class Base {
protected myMember;
}
class Derived extends Base {
foo() { return this.myMember; } // OK
}
var x = new Derived();
console.log(x.myMember); // Error, cannot access
class Derived2 extends Base {
private myMember; // Error, cannot tighten visibility
public myMember; // Error, cannot widen visibility
}
Open Questions
After the last design meeting, the following open questions remained:
Is 'sibling' property access allowed?
C# and other IL languages prohibit this pattern, but Java allows it:
class Base { protected x: string; }
class Derived extends Base {
foo(n: Base) {
// Not allowed: cannot reference protected member through base class reference
console.log(n.x);
}
}
See these links for reasoning
http://blogs.msdn.com/b/ericlippert/archive/2008/03/28/why-can-t-i-access-a-protected-member-from-a-derived-class-part-two-why-can-i.aspx
http://stackoverflow.com/questions/1904782/whats-the-real-reason-for-preventing-protected-member-access
Are protected
members subject to the "same-declaration" rule as private
members for assignability/subtyping?
private
members are considered equivalent for the purposes of assignability and subtyping if they are from the "same declaration". This isn't quite the rule you would want for protected members. Consider some classes:
class Widget {
protected inspector: WidgetInspector;
}
class SquareWidget extends Widget {
// Use a more-specific 'inspector'
protected inspector: SquareWidgetInspector;
}
class CircleWidget extends Widget {
// Initialize here
protected inspector: WidgetInspector = new WidgetInspector();
}
var w: Widget;
var c: CircleWidget;
w = c; // Allowed, or not?
If we took the verbatim "same declaration" rule from private
, this assignment would be disallowed because w.inspector
and c.inspector
come from different declarations. It's not reasonable to have this assignment be disallowed.
However, if we do not use the "same declaration" rule, then a SquareWidget
would be assignable to a CirceWidget
even if they both removed their extends
clauses. This is not surprising if you're used to thinking about things structurally, but since many people seem to like the higher specificity of private
in terms of preventing assignability between structurally-equivalent types, this behavior might not be desirable.
A proposed rule was that we could have a notion of a "parent" declaration when a derived class's property overrides a base class property. This seems tractable for classes, but interfaces can extend
multiple classes, and we would need to define what exactly that means. A degenerate example:
interface WatWidget1 extends Widget, CircleWidget { }
interface WatWidget2 extends Widget, SquareWidget { }
var ww1: WatWidget1;
var ww2: WatWidget2;
ww1 = new Widget(); // Allowed or not?
ww1 = new CircleWidget(); // Allowed or not?
ww2 = new Widget(); // Allowed or not?
ww2 = new CircleWidget(); // Allowed or not?
ww1 = ww2;
ww2 = ww1;
Can public properties be assigned to protected fields?
class Point1 { x: number }
var p1: Point1 = { x: 3 }; // Allowed
class Point2 { private x: number }
var p2: Point2 = { x: 3 }; // Disallowed
class Point3 { protected x: number }
var p: Point3 = { x: 3 }; // Allowed or not?
This is sort of a yes-or-no thing tangentially related to the previous question.
Great edit history on this one.
We should discuss this.
👍
👍
+1
+1 👍 , I just started with typescript, and protected visibility is a really big missing feature when you are creating an OO API.
@RyanCavanaugh Saw your tweet asking for feedback! Here are some thoughts:
Is 'sibling' property access allowed?
No. Aside from the other existing arguments, this doesn’t match what I expect protected
to do, which is to allow accessing/modifying inherited own members without exposing them publicly. When passing in an object to a class method, even if you “know” what the class is, they’re no longer your own properties, they’re some other object’s, and you shouldn’t be touching them.
I think this is an easy and safe way to go since if it’s absolutely necessary to access the other object’s protected properties for some reason without a compiler warning, there is still a way to get around it (all
type).
Are protected members subject to the "same-declaration" rule as private members for assignability/subtyping?
In the first example, I would expect w = c
to be allowed since CircleWidget is a compatible subtype of Widget. Intuitively, I would not expect CircleWidget to be assignable to a SquareWidget type, but I think this is one of the cases where the decision to use structural subtyping makes this counter-intuitiveness make sense for the language. (It feels roughly equivalent to the unintuitiveness of not being able to instanceof
a TypeScript type.)
Given the degenerate example, CircleWidget extends from Widget already so this declaration is effectively WatWidget extends CircleWidget
. In that case I would expect CircleWidget to be a compatible assignment but not Widget since the variable’s type is more specific than Widget. Hopefully that doesn’t contradict my other thoughts above :)
Can public properties be assigned to protected fields?
No. This goes back to the whole “only touch your own things” thing. I think it makes it easier to think of how you’d use properties like this if you were writing vanilla JS—they’d typically be underscored to annotate “protected”, and assignment to these properties from outside the constructor/prototype methods would typically be an illegal operation.
Hope that helps!
Oh, one other thought, since it was brought up above and so is I guess related—I don’t feel like private properties of an object should be considered in whether or not a type assignment is compatible, although I realise (only just) that they are right now. Only the public properties are what I would expect to be compared in assignment or passing, not private, not protected. I would only consider private/protected types in the inheritance system.
I don’t feel like private properties of an object should be considered in whether or not a type assignment is compatible
This isn't a good idea because assignability is what determines parameter validation, for example
class MyPoint {
private _length;
constructor(public x: number, public y: number) {
this._length = Math.sqrt(x * x + y * y);
}
differenceInLength(p: MyPoint) {
return p._length - this._length;
}
}
var regularPoint = { x: 3, y: 6 };
var myPoint = new MyPoint(10, 10);
// Desired: Error
console.log(myPoint.differenceInLength(regularPoint)); // No error, prints NaN
@RyanCavanaugh Well that differenceInLength
function is (in my mind) illegally accessing the private property of another object p
…so I wouldn’t expect it to work, I would expect the access of p._length
to be an error, per the rationale I gave to the first question. :) My opinion is object/API compatibility should be defined by the object’s public properties, and private/protected are used for self-implementation only. Obviously since we are dealing with JavaScript there are some restrictions here—if the language (JavaScript) itself implemented private types I would expect a subclass to be able to reuse the same property name for its own different private property, but that’s not exactly realistic when the compiled result is all public, all the time.
Anyway, again, it’s just my opinion and not necessarily the right choice for the language.
that differenceInLength function is (in my mind) illegally accessing the private property of another object p…so I wouldn’t expect it to work, I would expect the access of p._length to be an error
Yes, making this an error may also eliminate other corner cases that are at present preventing other issues from progressing, for example #471
Hmz If I understand the above correctly, I have to disagree on the p._length should be an error part, I believe that C# also allows accessing private members from other objects from the same type in the same class. (not sure how to say it correctly) but a mostly the same example from CyrusNajmabadi compiles fine in CSharp, which I think should compile in TypeScript too (which it does currently)
public class bar
{
private int foo;
void test(bar x)
{
// accessing private x.foo
Console.WriteLine(this.foo == x.foo);
}
}
On the same note that
var that = this; that._length;
should not be an error
(Or am I totally missing the point on your examples here?
@DickvdBrink, "because it works in C#" is not a very compelling argument IMO 😃 Why is it important to permit accessing private members of other objects of the same type _in JavaScript_? I'm sure it permits certain use-cases, but it also prevents others.
Unlike C# it's very easy to get around the error if people really want to:
class Bar {
private foo: number;
test(x: Bar) {
// accessing private x.foo
console.log(this.foo === x["foo"]);
}
}
if people really want to
@NoelAbrahams but wouldn't you loose type safety this way?
Let's not derail on whether or not you should be able to access private members of other instances of the same class. That ship has sailed and it's not changing at this point.
Let's not derail on whether or not you should be able to access private members of other instances of the same class. That ship has sailed and it's not changing at this point.
It’s your project, so I won’t say any more I guess, but if an existing feature is a barrier to implementing a new feature in a way that makes sense, I am not sure how it is derailing to look at that existing feature in light of the new feature. I don’t like ad nauseam discussions, but I do think that treating past decisions as sacrosanct may lead to an inferior design in this case.
I look forward to the outcome of this discussion.
Best,
@csnover - I'm not sure if I would characterize it as "treating past decisions as sacrosanct". We, and languages like C#, do breaking changes between versions to help fix up bits of the language. These are done very carefully, but they're still done. Each one gets weighed impact to existing users vs the value of the fix.
The trick with changes something like "can you access private properties of a instance of your type?" is that there are multiple ways we could have gone. C# lets you do the access, but like you pointed out there are also reasons to disallow it. We chose the design pivot that landed us with similar capabilities to C#, which a set of users are used to and no doubt have already built programs in TypeScript with the assumption this should be allowed.
While backward breaking changes do happen between versions, changing this falls more into the type of backward breaking change that has a large potential impact on users without a value that's strong enough to warrant it.
@jonathandturner, I have just deleted a labouriously constructed argument about breaking changes, because your answer just popped up in front of my eyes like magic.
All I have left to say is a rather pathetic, "where is the evidence for the following?"
changing this falls more into the type of backward breaking change that has a large potential impact on users
Just as a bit of background, we have a pretty sizeable suite of TypeScript code collected from inside and outside of Microsoft that we try out design changes on. This gives us a first approximation of "okay, say we change this, how much do we know will be immediately affected?" It's not perfect, but it does help us catch tricky errors that come from subtle interplay between design decisions earlier on, if possible.
Changing how private accessibility works breaks the assumptions people have coming from C++, C#, and I think Java also. Unsurprisingly, this also shows up in our test suite as hundreds of errors where people have assumed private would work the same way as the languages they are used to.
Just some selections from the OSS projects we index:
https://github.com/BabylonJS/Babylon.js/blob/master/Babylon/Actions/babylon.action.ts lines 55 and 56
https://github.com/BabylonJS/Babylon.js/blob/master/Babylon/Bones/babylon.bone.ts line 74
https://github.com/NTaylorMullen/EndGate/blob/master/EndGate/EndGate.Core.JS/Rendering/Camera/Camera2dCanvasContextBuilder.ts lines 43 through 60
https://github.com/sinclairzx81/appex/blob/master/node_modules/appex/web/Server.ts lines 255 through 260
https://github.com/sinclairzx81/appex/blob/master/node_modules/appex/workers/Worker.ts line 80
Ah, okay... so I learnt a few things today:
-
We chose the design pivot that landed us with similar capabilities to C#, which a set of users are used to and no doubt have already built programs in TypeScript with the assumption this should be allowed
-
we have a pretty sizeable suite of TypeScript code collected from inside and outside of Microsoft that we try out design changes
plus the actual evidence from Ryan, together makes for a better answer than the one involving ships 😃
Perhaps these should be committed to the design goals (as at present it says nothing about C# or breaking changes) in order to prevent discussions veering off in fanciful directions that are never going to be considered.
I’m not a language designer so I could be way off base, but the problem I see with any statement about relating to languages like C#, Java, etc. is that those languages use nominal type systems, not structural type systems. As a result, I don’t think it is possible to design TypeScript private/protected properties by copying patterns that work in these other languages’ type systems, since those patterns don’t really seem to mesh with the design of TS. I feel like the questions raised by the OP verify this situation, but I would be interested to see what design approach could be taken that isn’t confusing or impossible that doesn’t conform to my current thoughts/feedback.
The design goals now go up to 11 🎸
C# is not an explicit design target (see non-goal #1). Rather, Jonathan's statement was that our behavior for private
is broadly in line with other languages (C++, Java, Python, PHP, Objective C, VB.Net, Swift, etc). In general, it is going to be better to use behavior that works the same as it does it other commonly-used languages, especially if the majority of languages agree on that behavior. It improves predictability for people new to the language, and it's usually the case that everyone is making the same decision for a good reason. That doesn't mean we're never going to deviate (putting types on the right-hand side of an identifier, for example), but those kind of choices should be made with clear reasoning.
With that said, let's please talk about protected
😕
Non-goal number 7:
Introduce behaviour that is likely to surprise users. Instead have due consideration for patterns adopted by other commonly-used languages.
🎺
👍 to @NoelAbrahams 's suggestion for non-goal 7
Closed in #688.
I realize this is probably already set in stone and I apologize for digging up a bones, but I wanted some clarifications on the decision that was made for "Is 'sibling' property access allowed?"
To me, it makes sense that siblings can use each other's protected methods if the protected method is defined in a common parent (super) class. In effect, I view protected methods as creating an interface (as opposed to defining an implementation detail) that can be viewed by ______ (the blank space is specific to the language, in TypeScript's case, it would be the inheritance chain).
In effect, this is how I think about it: The sub classes know that they have to implement the interface defined by the super class. Similarly, they know that all their siblings have to do the same in order to be an instance of the parent class. They can see the protected method from their parent, so it must exist on the siblings. Since these classes know this, it doesn't seem like its breaking encapsulation by allowing siblings to access protected methods. I place emphasis to indicate that the protected method is not optional and that it is a known fact by all siblings that it exists. This is different than private methods in which case the existence of the methods are unknown and therefore unusable outside the class.
I ask because I'm designing some classes using a composite pattern. One of the protected methods for the composite object is supposed to iterate over the objects it contains and return a composite value. The method is defined as protected in the super class and therefore known to exist by the composite object. Obviously I could easily make the method public, but following the principle of least privilege, it makes sense to make it protected since this method is only used within the inheritance chain.