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

Suggestion: 'protected' modifier #1

Closed
RyanCavanaugh opened this issue Jul 14, 2014 · 27 comments
Closed

Suggestion: 'protected' modifier #1

RyanCavanaugh opened this issue Jul 14, 2014 · 27 comments
Assignees
Labels
Committed The team has roadmapped this issue Suggestion An idea for TypeScript

Comments

@RyanCavanaugh
Copy link
Member

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.

@RyanCavanaugh
Copy link
Member Author

Great edit history on this one.

We should discuss this.

@philipbulley
Copy link
Contributor

👍

1 similar comment
@basarat
Copy link
Contributor

basarat commented Aug 11, 2014

👍

@diverted247
Copy link

+1

@galloscript
Copy link

+1 👍 , I just started with typescript, and protected visibility is a really big missing feature when you are creating an OO API.

@ahejlsberg ahejlsberg self-assigned this Sep 15, 2014
@csnover
Copy link
Contributor

csnover commented Sep 15, 2014

@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!

@wavebeem
Copy link

@csnover 👍

@csnover
Copy link
Contributor

csnover commented Sep 16, 2014

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.

@RyanCavanaugh
Copy link
Member Author

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

mhegazy pushed a commit that referenced this issue Mar 31, 2015
mhegazy pushed a commit that referenced this issue Dec 8, 2015
cspotcode added a commit to cspotcode/TypeScript that referenced this issue Oct 25, 2017
cspotcode added a commit to cspotcode/TypeScript that referenced this issue Oct 25, 2017
@c1moore
Copy link

c1moore commented Jan 16, 2018

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.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Committed The team has roadmapped this issue Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests