microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.8k stars 12.46k forks source link

[feature] class properties that are "readonly in public, writable in private" or other permutations #37487

Open trusktr opened 4 years ago

trusktr commented 4 years ago

Search Terms

typescript readonly on the outside writable on the inside

Suggestion

Some sort of syntax to describe readonly on the outside, writeable on the inside for a given property, so we can avoid making a getter just for this purpose (or using any of the convoluted options linked in that StackOverflow post).

Use Cases

To make this pattern easier to express.

Examples

instead of having to write

class Foo {
  private _foo = 123
  get foo() {
    return this._foo
  }

  changeIt() {
    this._foo = 456
  }
}

const f = new Foo
f.foo = 345 // ERROR

we would be able to write something shorter like

class Foo {
  publicread foo = 123

  changeIt() {
    this.foo = 456
  }
}

const f = new Foo
f.foo = 345 // ERROR

Checklist

My suggestion meets these guidelines:

RyanCavanaugh commented 4 years ago

Previously at #2845 but we should revisit after so long

trusktr commented 4 years ago

Just a note, but #2845 is similar but different. This one is not about making a getter/setter, but about making only a property, and being able to specify that the property be readonly on the outside of the class, and writable only inside the class (and subclasses too, depending on the keyword used)

Taking the idea a little further:

class Foo {
  // readonly in public code or subclasses, writable in this class only
  publicread privatewrite foo = 123

  // readonly in public code, writable in this class or subclasses
  publicread protectedwrite bar = 123

  // not visible in public code, readonly in subclasses, writable in this class only
  protectedread privatewrite baz = 123
}

and otherwise public is equivalent to publicread publicwrite, protected is equivalent to protectedread protectedwrite, and private is equivalent to privateread privatewrite, where the write specifiers can not be less strict than the read specifiers (or can they?).

woubuc commented 4 years ago

I'd love this feature in Typescript. It would allow me to simplify so many of my classes, especially in libraries and packages that expose a public API.

I'm not a huge fan of the publicread privatewrite syntax proposed by @trusktr as those keywords feel a little 'forced' to me. How about using the existing readonly keyword and allowing multiple access modifiers in increasing order of specificity (public > protected > private). Access is then resolved from right to left, with the first matching access modifier defining the access level and readonly status:

// Seen as `private foo` by class members
// Seen as `public readonly foo` by other code
public readonly private foo : Foo;

// Seen as `private foo` by class members
// Seen as `protected readonly foo` by subclass members
// Not accessible by other code
protected readonly private foo : Foo;

// Seen as `protected foo` by class and subclass members
// Seen as `public readonly foo` by other code
public readonly protected foo : Foo;

// Would technically be valid as well, though not very useful
// Seen as `private readonly foo` by class members
// Seen as `protected foo` by subclass members
// Seen as `public readonly foo` by other code
public readonly protected private readonly foo : Foo;

// Throws an error on compile, since the `private` modifier is more specific than `public`
private public readonly foo;

The upside of this would be that we don't introduce new keywords, and the syntax is flexible enough in case other modifiers like readonly are added.

A downside would be that function signatures like these are a little harder to parse visually when reading the code. But then the same could be said for everything that adds more keywords.

A question here would be how to handle the 'no access modifier = public' rule that Typescript uses. Should the public modifier be required for signatures with multiple access modifiers, or would the following also be valid?

readonly private foo : Foo;

I'd argue that this could get confusing (was this intentional or did someone mix up the order of keywords?) so I think an explicit public modifier should be required.

Note: this is written by someone who has no knowledge of the Typescript compiler or language design in general. So I don't know how easy/difficult/impossible this would be to implement. I'm curious to get some feedback on the syntax and on why this is or isn't a good idea.

robbiespeed commented 4 years ago

Just encountered a use case for this today when writing out a value ref class.

class Ref <T> {
  readonly value: T;
  constructor (value: T) {
    this.value = value;
  }
  set (value: T) {
    this.value = value; // errors unless I override the type of `this`
    return value;
  }
  // ...
}

Overriding the type of this, can get annoying fast if you have lots of methods that need write access.

There are other ways around it like storing the value in a private field then creating a getter, but that adds a performance penalty.

@trusktr It might be best to default reads to always be public.

// Seen as `private foo` by class members
// Seen as `public readonly foo` by other code
privatewrite foo: string

This goes along with how typescript works now, fields are by default public and you narrow their scope using private or protected. My guess is it would also be easier to accommodate most cases without the need to explicitly define both read and write.

Also here's how I am getting around this issue right now. It's a bit cumbersome, but maybe it will help out others who desire this feature.

type Writeable <T> = {
  -readonly [P in keyof T]: T[P];
};

type WritebaleRef <T> = Writeable<Ref<T>>;

class Ref <T> {
  readonly value: T;
  constructor (value: T) {
    this.value = value;
  }
  set (this: WritebaleRef<T>, value: T) {
    this.value = value; // no error because `this` type is changed to WritebaleRef<T>
    return value;
  }
  // ...
}
yvbeek commented 4 years ago

This is one of the features I miss so much in TypeScript.

I don't really like the publicread privatewrite notation. Is there an other language that uses that notation?

Here are a few suggestions:

// Option 1: C# style
public name: string { get; private set; }

// Option 2: Swift style
private(set) name: string

// Option 3: Swift struct-style
public readonly name: string

mutating changeName(name: string) {
  this.name = name
}

// Option 4: New keyword
public frozen name1: string
public readonly name2: string

My choice would be the Swift style (option 1) or C# style (option 2). Option 3 is for if we can't change the property definition.

woubuc commented 4 years ago

I also like the C# syntax but I think it's too far from the way getters and setters are defined in JS/TS so it would look out of place. Swift syntax looks nice though.

trusktr commented 4 years ago

Neat to know how other languages do this.

@woubuc Your idea is great if we need to prevent from adding new keywords.

With the new keywords I suggested we can do things like

publicwrite protectedread privatewrite foo = 123

which makes foo writable only in public and private code, but not in subclasses (it could be a valid use case, f.e. "I want this class to handle user input for foo, but I don't want subclasses to interfere with user input of foo, while the subclasses can still extend functionality in other areas").

@woubuc How would we specify the same thing with the space-separated existing keywords? Would it be

public protected readonly private foo = 123

?

Also with the new keywords, order wouldn't matter (or at least, the intention is clear regardless of order):

privatewrite protectedread publicwrite foo = 123

Here's my attempt with the existing keywords:

private protected readonly public foo = 123

But with the new keywords, what would happen if someone tries to use the existing keywords?:

privatewrite protectedread protected publicwrite  foo = 123

Perhaps they'd need to be mutually exclusive, so we can either use only the existing keywords, or only the new ones, but not both.

whzx5byb commented 4 years ago

The Readonly<T> generic type (or an interface) will do the "public readonly, private writable" pattern.

interface IFoo {
  readonly foo: number
}
class Foo implements IFoo {
  foo = 123

  changeIt() {
    this.foo = 456
  }
}

const f: Readonly<Foo> = new Foo
const g: IFoo = new Foo
yvbeek commented 4 years ago

@whzx5byb I appreciate your example, but it is not a solution to the problem.

  1. the compiler would still allow me to create an instance of Foo and then set the foo variable
  2. this forces the developer to use interfaces instead of classes
  3. this becomes impractical in more complex constructs like inheritance
paul-marechal commented 4 years ago

I'm not sure overly precise granularity is useful if it makes us write crazy things to define properties?

I think something simple like the following would do the trick:

class Foo {
  /**
   * foo can be read from public, protected and private contexts,
   * writable only within the class and its subclasses.
   */
  public protected foo: number;
}

class Bar {
  /**
   * bar can be read from public, protected and private contexts,
   * writable only within the class.
   */
  public private bar: number;
}

class Buzz {
  /**
   * buzz can be read from protected and private contexts,
   * writable only within the class.
   */
  protected private buzz: number;
}

Adding "larger" modifiers would only expand readability, and writabiIity would be kept to the most restricted modifier. We can finally add the readonly keyword in this word soup to close this last scope and never be able able to write.

edit: Actually you would never need to mix the readonly keyword with more than one access modifier.

I don't really see a case where someone would want it the other way around, like "public read/write and protected readonly".

trusktr commented 3 years ago

"public read/write and protected readonly".

@marechal-p The example I showed above was "public read/write, protected readonly, private read/write". In that concept, this allows the public (end users) to pass data in, and only allows the private scope to have full control (subclasses aren't afforded this).

With your idea (which I do like as a subset and feel that it would already be a lot better than what we currently have), how could we implement similar? Seems impossible.

I think being able to distinguish public input from protected input (in the private scope) could be useful. But I would totally settle with your idea over the current.

paul-marechal commented 3 years ago

In that concept, this allows the public (end users) to pass data in, and only allows the private scope to have full control (subclasses aren't afforded this).

My opinion here is that subclasses should be seen as "end users" hence why I found that this was odd. In your example the field should simply be public (r/w in public/protected/private contexts) because if anyone can change the value from outside then the class private implementations cannot make assumptions about it: it will be mutated by random clients, in public or protected scopes, doesn't matter.

My proposal fits the issue's title "readonly on the outside, writable on the inside" and allows one to avoid the private variable with public getter pattern with minimal syntax modification:

// useless boilerplate, runtime implications:
private _a
public get a() {
    return this._a
}

// less runtime shenanigans
public private a

It is my belief that doing like your example proposes and having "public r/w, protected read-only, private r/w" is unsound. I have a hard time imagining an API design that would need such specificity, and I would assume the design itself needs to be changed.

snarfblam commented 3 years ago

It is my belief that doing like your example proposes and having "public r/w, protected read-only, private r/w" is unsound. I have a hard time imagining an API design that would need such specificity, and I would assume the design itself needs to be changed.

Protected read-only and yet public read/write is an oxymoron. I have to imagine public accessibility must be a strict subset of protected accessibility, which in turn must be a strict subset of private accessibility. Anything else is, on a conceptual level, unenforceable (e.g. you can easily and unintentionally circumvent such an access restriction via helper function).

As far as syntax, I'm not a huge fan of public private myProperty. I think the word readonly should be in there since that's what we're dealing with. I'd vastly prefer what @woubuc suggested. This reads more naturally and clearly.

    // Publicly read-only, otherwise protected.
    public readonly protected foo: Bar;

    // Protected read-only, otherwise private.
    protected readonly private foo: Bar;
Griffork commented 3 years ago

:+1: for this, and I like the swift style as the most readable (it's also not too verbose).

Putting multiple of the same modifier in a row, as public private or public readonly private is a recipe for disaster imo. My problems with this are:

Reasons I like the swift style:

The downsides to using the swift style is that private(get) property would be an alias for private readonly property (not a big problem, but slightly confusing for new coders).

snarfblam commented 3 years ago

Putting multiple of the same modifier in a row, as public private or public readonly private is a recipe for disaster imo. My problems with this are:

* There's no way people would understand what's written unless they read the specific section in the docs that discribes what multiple public/private modifiers mean.

public private seems confusing for obvious reasons. public readonly private might give you a moment of "heywhat" the first time, but I only see one way you can reasonably interpret it. If you guess, you may not feel confident in your guess, but it will almost certainly be correct. As far as googling it, anyone trying to program should have the know-how to use quotes with Google. (Guess what the very first result for "public readonly private" is.)

Nonetheless, it may not be ideal. Just my personal preference. There are plenty of other options. public get private set seems pretty straightforward to me. I do grimace at suggestions involving punctuation. Javascript already tends to get dense with parens and braces. The further you move from having them depict the structure of your logic, the harder everything is to parse. Not to mention Google tends to ignore punctuation in searches, even inside quotes.

SrBrahma commented 3 years ago

This is one of the features I miss so much in TypeScript.

I don't really like the publicread privatewrite notation. Is there an other language that uses that notation?

Here are a few suggestions:

// Option 1: C# style
public name: string { get; private set; }

// Option 2: Swift style
private(set) name: string

// Option 3: Swift struct-style
public readonly name: string

mutating changeName(name: string) {
  this.name = name
}

// Option 4: New keyword
public frozen name1: string
public readonly name2: string

My choice would be the Swift style (option 1) or C# style (option 2). Option 3 is for if we can't change the property definition.

I think the 2nd one is really good and wouldn't break existing code and it basically don't add new keywords.

This issue should really be worked on.

grind-t commented 3 years ago

If you perform a cast, then the value can be changed (playground), although it looks a little strange.

mixtur commented 3 years ago

...public accessibility must be a strict subset of protected accessibility, which in turn must be a strict subset of private accessibility

This shaped the following model in my head

If you add to the cited prospsition that allowing write implies allowing read you can make a list like this. Here every "capability" below "code" is alowed and everything above is forbidden

capability code
public
public write
public(get)
public read
protected
protected write
protected(get)
protected read
private
private write
private(get)
private read
paul-marechal commented 3 years ago

@mixtur So would your proposition look like the following?

class Foo {
    public(get) protected name: string;
}
mixtur commented 3 years ago

@marechal-p

@mixtur So would your proposition look like the following?


class Foo {
   public(get) protected name: string;
}

No. Just

class Foo {
    public(get) name: string;
}

This would mean that the only forbidden thing is public write.

Ow. I see your point. So one actually might want to forbid writing from both protected and public, but allow reading for everyone.

paul-marechal commented 3 years ago

@mixtur my point was just to see how it plays out... To expand more: with your notation, if we want something to be protected but allow reading from public we would just write public(get) and nothing more? Felt weird to omit the protected keyword.

mixtur commented 3 years ago

More than two keywords feels noisy.

I though about it and I was actually wrong with my model. It doesn't work like a "list". I drew this chart. Cases 4 and 5 are falling out of the list representation.

image

So according to this I suggest

case 1 - public(get) case 2 - protected(get) case 3 - private(get)

cases 4 and 5 I think are rare so it is fine for them to require more noisy syntax.

case 4 - public(get) private(set) case 5 - protected(get) private(set)

robbiespeed commented 3 years ago

cases 4 and 5 I think are rare so it is fine for them to require more noisy syntax

@mixtur case 4 is what inspired this proposal, so I disagree that it would be rare I think it would be the most used of any of the new cases, followed by case 1, then 2.

Case 3, and 5 already have working syntax: case 3 - private readonly case 5 - protected readonly

My syntax suggestion for the additional cases would be: case 1 - protected read(public) case 2 - private read(protected) case 4 - private read(public)

Where read([public|protected]) must follow [protected|private] similar to readonly but instead of removing write privilege it elevates read.

Case 1 and 4 could be further simplified if read implied read(public): case 1 - protected read case 4 - private read

paul-marechal commented 3 years ago

Like @robbiespeed mentioned, cases 3 and 5 are already supported.

Then I'd go with either A:

case 1. public readonly protected case 2. protected readonly private case 4. public readonly private

Or either B:

case 1. public protected case 2. protected private case 4. public private

Griffork commented 3 years ago

I'm quite a fan of using the get and set words instead of read and write because in my mind it maps better to getters and setters having different permission levels than the readonly keyword. It will also gel well with the (presumably also provided capability) of having getters and setters with different access levels, so public private(get) would be the same as:

private get () {...}
public set () {...}

Again @marechal-p don't use "protected readonly private" because it's not at all intuitive. Cases can be made for why read is protected (because it's preceeded by the protected modifier which is how the rest of typescript works) or how read is public (because public is a modifier on the readonly keyword). The answer "well if they look it up in the docs they'll know" is not good enough when we have proposals that are not confusing.

Also I agree with @mixtur about the syntax for case 4 and 5, not because it's rare but because it's very easy to understand. I'm currently in the process of onboarding several juniors and I'd rather not have the "I wanna type less characters" argument cause a ton of extra work for me.

paul-marechal commented 3 years ago

@Griffork currently in TypeScript readonly must follow the access modifier. So protected readonly private following current TypeScript conventions would mean that readonly applies to protected, with the added r/w access from private contexts. I don't think it's any more confusing than seeing things like:

class Foo {
    constructor(readonly bar: string, baz: readonly string[]) {}
}

Some things are learned once and never forgotten. I only keep proposing this syntax because it doesn't add new keywords, just a different way to interpret it. I also think that ordering of the keywords should be important, with the most public access modifier on the left, and the most private on the right. So syntax like protected read(public) feels counter intuitive to me, but that's personal.

Building on @mixtur proposed syntax, I could see something like:

case 1. public(get) protected (or just public(get)?).

In this case with public(get) only, one issue for first time users would be to wonder how it is different from public readonly which doesn't grant you write access from other scopes. Either something to be learned from docs or use public(get) protected.

case 2. protected(get) private or just protected(get). case 3. private readonly already supported by TypeScript. case 4. public(get) private

This last case should be the reason why using public(get) alone would be ambiguous, so it seems better to always use public(get) (protected|private) here.

case 5. protected readonly already supported by TypeScript.

My only concern with this syntax is how it doesn't feel consistent with what is already supported in TypeScript (cases 3 and 5). My proposed syntax at least stays somewhat consistent.

@robbiespeed syntax seems unambiguous, but I'm just not a fan of the new read keyword. But if we re-use readonly we end up with things like protected readonly(public) and at this point I wouldn't find public readonly protected much more confusing.

But in the end no matter what the syntax ends up being I won't be too picky anyway, as this is a feature I had a use for.

Griffork commented 3 years ago

@marechal-p I'm going to continue bikeshedding on this issue just for a bit.

It's may be obvious to you but that doesn't mean it's going to be obvious to other people. Just because you have a reason in your head as to why it makes sense doesn't mean everyone else is going to come to the same conclusion. For example it's not obvious to me and I've been using Typescript since 0.8 and consider myself to be very proficient with it. I don't think I'm particularly stupid or abnormal here, I think I'm just coming from a different place with different expectations and a different thought process, which is why I'm so adamant that this is a bad idea. And if I can come to the wrong conclusion, won't other people also have the same problem?

Yes I understand it after I've had it explained to me. Yes I think I'd get confused multiple times while using/reading it before it actually sticks. Yes I think people with no prior Typescript experience coming from other languages (C#, python or Javascript etc) would be confused reading this syntax.

I am already spending a lot of time teaching people typescript syntax because it's so different to other languages, I'm not in favour of using "existing keywords" when a much clearer alternative is available.


On another note, we need a new keyword for setting anyway don't we? I was thinking you could have a public set and private get - but is that even possible or is there a way to read a value through the process of setting it?

yvbeek commented 3 years ago

@Griffork How would you implement a property that can set from the outside, but that can be modified inside of the class? I think the only viable solution right now would be to shadow that property with a private field. Which adds a lot of noise to the code.

Griffork commented 3 years ago

@yvbeek You can't, but this proposal would enable it. Remember that there's runtime no implementation difference between a variable that's readonly and a variable that's not. Both can be set at runtime, it's just a matter of how the compiler responds to it. If we're splitting get and set's private/protected/public access level I see no reason why set can't have a more accessible level than get.

Unless there is a way to read the value of the variable via setting it (e.g. by setting it to itself) that would not be able to be detected and marked as an error by the compiler.

paul-marechal commented 3 years ago

I see no reason why set can't have a more accessible level than get.

@Griffork How about nonsensical design? Here it is more a question of semantic rather than just syntax, and it was discussed before:

...public accessibility must be a strict subset of protected accessibility, which in turn must be a strict subset of private accessibility

Currently when you write public a it is implied that both protected and private scopes inherit the same accessibility. Write protected a and now only the private scope has the same access. Allowing something to be settable from public scope but not protected nor private doesn't seem to make much sense. Because we can doesn't mean we must.

mixtur commented 3 years ago

Ok. Another proposal. (And another image)

In the following image columns are for read (public, protected, private), rows are for write (public, protected, private, constructor only)

image

Cases 6, 7 and 8 are making no sense. Because you know what you just wrote anyway, so why forbid reading it.

Things are implicitly public in typescript:

a = 1;          // is the same as public a = 1;
readonly b = 2; // is the same as public readonly b = 2

I think this should continue to be the pattern. So the syntax could be

write protected a = 1;
// the same as
public write protected b = 2; // case 1

write private b = 2;
// the same as
public write private b = 2;   // case 4

protected write private     // case 2

Another idea. When you allow writing only for the class itself (third row) you make the class responsible for contents of the field or make it the "owner" of the field. So why not just

own a = 1;
// the same as
own public a = 1;    // case 4

own protected b = 2; // case 2

Not sure what to say about case 1 though.

paul-marechal commented 3 years ago

Not sure what to say about case 1 though.

public readonly which can be set from the constructor only.

edit: So that was a brain fart. case 1 isn't already implemented by public readonly as it prevents writing outside of the constructor in protected and private scopes. public protected would be my preferred writing here.

trusktr commented 3 years ago

Allowing something to be settable from public scope but not protected nor private doesn't seem to make much sense. Because we can doesn't mean we must.

@marechal-p If you mustn't, then definitely don't (we have linters that can help with that). Some people don't like that idea, but some do.

These are those cases (if you would like to add them to your pictures that'd be great @mixtur):

-- read write
public yes yes
protected yes no
private yes yes
-- read write
public yes yes
protected yes no
private yes no
-- read write
public yes yes
protected yes yes
private yes no

There are two ways to think about it:

1)

Some people believe that anything outside a base class that has a private property is effectively public to that base class (regardless if the outside code is in a subclass (protected), or outside of any class (public)).

This is valid, and there is code written this way.

This set of people may believe the above use cases in the tables are oxymorons (they can disable features they consider oxymoronic with a linter).

2)

Some people believe that conveying intention and semantics between groups of people in certain cases is important.

As an example, a base class author on a team may write a base class that has a variable that should be read/write in both private or protected scope (for example), and wishes for subclass authors to only react to changes of the variable value, and wishes for non-class authors to read/write to the variable so as making that variable a source of input.

Group of people 1: The base class author, and authors of sub classes, all of which may be on the same team. Or maybe even other people outside the team following the team's conventions. These people may, for example, be writing Custom Elements (Web Components) that inherit a particular library feature from a base class, and subclass authors wish to follow the base class convention of allowing only DOM reference users to provide values from outside.

Group of people 2: There are a group of people that will be getting references to DOM elements (for example), and they want to know what they can write to. They see that some property is read/write, and they understand it is part of the API that they use in the case of grabbing elements from the DOM and working with them.

Group 1 (which includes the base class authors, and subclass authors) wants to ensure that the outside (public) user's view of an object's (element's) state is consistent with their own outside (public) manipulations. The (base and sub) class authors know that if they modify a variable intended as input from DOM manipulators (outside public users) in Group 2, those users may get confused.

In the Custom Element (Web Component) world, it is a good practice not to modify attributes from the inside, because it ruins the outside user's expected contract. This is a known good practice in DOM world.

To express this good practice, it is necessary to mark things as readonly inside, and writable outside, where readonly inside may be in both protected and private scope, or it may be read only in protected scope while, for example, a base class where the property originates (private) needs to manipulate the variable during construction based on other inputs.

This is a valid use case that in practice exists (f.e. Custom Element / Web Components community).

I know there are different ways to implement these patterns, but this may offer a more concise method of doing it.


The main point of the use cases supported by those tables I added is supporting semantics conveyed to groups of people depending on which part of development they participate in.

In the Custom Elements case I described, the base classes have a contract with protected classes: to the base class with state that is reasonly to protected, the protected subclasses are not public code, they are collaborators following conventions.

Griffork commented 3 years ago

Some people believe that conveying intention and semantics between groups of people in certain cases is important.

This is the purpose of typescript is it not? Private variables are not really private, types aren't enforced and any function can be overridden at run-time, typescript just says "please don't do this". The whole point is to be able to convey to your team and/or to library users both what to expect and how they should interact with your code.

I love the example which really helps, I was arguing from a purist point of view, which is typescript is designed to describe (with typing support) anything you can do in javascript. The design shouldn't be intentionally restricted by the designers just because you can't think of a use-case there and then (although @trusktr has provided a nice example) it should be as open and broad as is possible while being implementable, easily comprehendable and fitting what javascript can do.

Having said that the current proposal doesn't support what you're suggesting @trusktr. Can you provide an example of what your ideal syntax would look like in all three of the cases you list?

Jet132 commented 3 years ago

Would the syntax I proposed in #43553 maybe be a solution? (please also read the first comment I left afterwards) It also includes cases where you might want to only expose the readonly counterparts (like ReadonlyArray instead of Array).

It doesn't cover cases 6, 7, and 8 but those seem questionable in usability to me anyway.

paul-marechal commented 3 years ago

@Jet132 I would prefer your proposition over anything I've seen here since it enables us to re-specify types.

I can see a case where you privately handle a concrete type, but publicly expose an interface.

Griffork commented 3 years ago

Actually I like that proposal better too.

yvbeek commented 3 years ago

I can see the benefit of being able to re-specify a type.

I don't really like that you have to define the name of the variable twice, that seems very error prone. If you forget to rename one of the two, or a silly mistake happens during a merge, you could run into some issues.

We could use a separate keyword for the field that shadows the original field:

public readonly prop1: ReadonlyArray<unknown>;
private shadow prop1: unknown[] = [];

That way you can raise an error when a "shadow" property exists without the original property.

khokm commented 3 years ago

Any progress on this?

AndreThompson-Atlow commented 3 years ago

I have a pretty big use case for this right now-- especially when it comes to unit tests. I'd like to make all of this stuff readable for the unit tests so I can check results, but keep it private in terms of actual functional code...

Jet132 commented 3 years ago

I have a pretty big use case for this right now-- especially when it comes to unit tests. I'd like to make all of this stuff readable for the unit tests so I can check results, but keep it private in terms of actual functional code...

This feature would not address your problem. It does not add more visibility filters than the existing private, protected, and public.

ifeltsweet commented 3 years ago

This is a big one for us as well. Currently, we do a lot of the following:

type Mutable<T> = {
  -readonly [k in keyof T]: T[k];
};

function mutable<T>(value: T): Mutable<T> {
  return value;
}

class Order {
  readonly price: number;

  applyDiscount(discount: number) {
    mutable(this).price = this.price - discount;
  }
}

I would like to see something like protectedwrite or guarded as I think it's not too ambiguous, googleable, and solves for the requested use case:

class Order {
  // Option1: can write from this class and subclass, access from anywhere
  protectedwrite price: number;

  // Option2: same as above, but with a "nicer" name
  guarded price: number;

  // Same as above, but explicit public access
  public protectedwrite price: number;

  // Compiler/linter warning because protectedwrite has no effect
  protected protectedwrite price: number;

  // Compile error because readonly overrides protectedwrite
  readonly protectedwrite price: number;

  // Compile error because private overrides protectedwrite
  private protectedwrite price: number;
}

All other variations like privateread would be quite unnecessary in my opinion and are solving for a completely different use case than what was originally requested by this issue.

snarfblam commented 3 years ago

I'm finding that template literal types are providing a potential workaround. Here's what I was able to come up with. It may need additional massaging and/or boilerplate to work for non-trivial use cases, but makes asymmetrical accessibility declarative and maintainable. In short, a helper type, GuardedAccess<T>, that makes any underscore-prefixed property read-only.

Here are the definitions:

// Identify property names prefixed with an underscore
// (or any template of your choice--a suffix works too).
type GuardedName = `_${string}`;
type GuardedNameOf<T> = keyof T & GuardedName;

// Separate guarded properties from unguarded properties
type GuardedProperties<T> = Pick<T, GuardedNameOf<T>>;
type UnguardedProperties<T> = Omit<T, GuardedNameOf<T>>;

// Join the guarded and unguarded properties, making the guarded properties read-only
export type GuardedAccess<T> = UnguardedProperties<T> & Readonly<GuardedProperties<T>>;

// A publicly exposed constructor will need to return the guarded type
interface GuardedConstructor<T extends {new(...args: any): any}> {
    new(...args: ConstructorParameters<T>): GuardedAccess<InstanceType<T>>;
}

// It may also be desirable to remove the access guard (e.g. for private access in a derived class)
type Mutable<T> = {-readonly[P in keyof T]: T[P] }; // Helper
type UnguardedAccess<T> = UnguardedProperties<T> & Mutable<GuardedProperties<T>>;
interface UnguardedConstructor<T extends {new(...args: any): any}> {
    new(...args: ConstructorParameters<T>): UnguardedAccess<InstanceType<T>>;
}

Basic usage is pretty straightforward.

// A class that declares guarded properties
class Demo {
    normal: string = "public read, public write";
    _guarded: string = "public read, private write";
}

// Share it with the world
export type DemoPublic = GuardedAccess<Demo>;
export const DemoPublic: GuardedConstructor<typeof Demo> = Demo;

Of course, the whole point is that guarded properties can not be publicly mutated.

// An example demonstrating that publicly writing a guarded property is an error
const example = new DemoPublic();

example.normal = "No error :)";
example._guarded = "Cannot assign to '_guarded' because it is a read-only property.(2540) :D";

The UnguardedConstructor<T> helper can be used when inheritance is needed.

// Reverse the process to get an inheritable class
const DemoPrivate: UnguardedConstructor<typeof DemoPublic> = DemoPublic;

// The subclass itself is business as usual
class DemoSubclass extends DemoPrivate {
    _alsoGuarded: string = 'public read, private write';

    constructor() {
        super();
        this._guarded = "No error :)";
    }
}

// Create our public interface
export type DemoSubclassPublic = GuardedAccess<DemoSubclass>
export const DemoSubclassPublic: GuardedConstructor<typeof DemoSubclass> = DemoSubclass;

Obviously there is room for improvement here but it may be a solid starting point. This was entirely off-the-cuff in the Playground so it hasn't been put through its paces. I quite like that we can declare publicProp, _publicReadonlyProp, and #privateProp with consistent syntax. Again, if the unconventional use of the underscore prefix rubs you the wrong way, you can use any template that suits you.

One downside here is that the intellisense isn't quite as pretty as you might like :/


Edit: Predictably, there are complications when extending an UnguardedConstructor<T>. Protected properties are lost, and methods become properties which interferes with overrides. So if you need to extend a class, you may need to make the original unguarded class available. I imagine there is probably a way to sneak the original constructor type along for the ride to be retrieved back out with a helper type, provided I can work out the necessary type wizardry.

snarfblam commented 3 years ago

Okay, this seems to be shaping up.

Typescript Playground

Sadly, this can only offer public read protected write. Mapped types don't offer a way to enable public read private write or protected read private write in this manner, but I've managed to address static members and added an optional mechanism to make the original types retrievable from the guarded versions for the purpose of inheritance.

class Demo {
    public unguarded = 'public read, public write';
    public _guarded = 'public read, protected write';

    public static unguardedStatic = 'public read, public write';
    public static _guardedStatic = 'public read, protected write';
}

//
// Make it safe for public consumption.
//

// Instance type
export type DemoPublic = Guarded<Demo>;

// Constructor function. The `true` argument indicates the guard may be reversed
// for inheritance. Specify `false` or omit it if this is not desirable.
export const DemoPublic = guardClass(Demo, true); 

// Alternatively, directly use either GuardedConstructor or 
// ExtensibleGuardedConstructor
export const DemoPublicAlt: ExtensibleGuardedConstructor<typeof Demo> = Demo;

// Unguard the class to extend it
class DemoSubclass extends unguardClass(DemoPublic) { }

// Again, you can use the helper types directly
const DemoUnguarded = DemoPublic as OriginalConstructor<typeof DemoPublic>;
class DemoSubclassAlt extends DemoUnguarded { }

You can also specify a different template for guarded identifiers as well.

type DollarGuard = `${string}$`; // dollar suffix
class Demo {
    public unguarded = 'public read, public write';
    public guarded$ = 'public read, protected write';
}

export type DemoPublic = Guarded<Demo, DollarGuard>;
export const DemoPublic:
    GuardedConstructor<typeof Demo, DollarGuard> = Demo;

One other workaround I've come across is using an abstract base class (an intermediate class in the case of inheritance) to declare an abstract accessor property, which permits asymmetric accessibility. When you extend the abstract class, you can use a data property as an implementation.

abstract class AsymmetricBase {
    public abstract get guarded(): string;
    protected abstract set guarded(val: string);
}

class Asymmetric extends AsymmetricBase {
    guarded = "public read protected write";
}

const test = new Asymmetric() as AsymmetricBase;
// No error.
console.log(test.guarded);
// Property 'guarded' is protected and only accessible within class 'AsymmetricBase' and its subclasses.(2445)
test.guarded = 'asdf'; 

Again, there are limits on which modes of accessibility it can permit. Again, you'll probably want some utility types to help you pull your type shenanigans. I'm not really sure how much practical utility this solution provides.


Of course, ideally, the language would simply just support this. Since accessor properties and data properties are functionally equivalent in terms of typing, and asymmetric accessibility is permitted with accessor properties, there's really no argument to be had against offering the same support for data properties. At this point I don't even want to argue about syntax.

Griffork commented 3 years ago

A bit out of the scope of this thread, but it'd be great to be able to apply protected write to module variables as well, so they're readonly externally but can be altered internally.

ExE-Boss commented 3 years ago

Note that exported module variables are already protected write when using native ES modules, since the module namespace exotic object behaviour provides a read‑only view of the exported variables.

snarfblam commented 3 years ago

That applies to the exported variables themselves (i.e. the exported reference to the class definition). That's unrelated to instance and static properties of a class.

thw0rted commented 2 years ago

I don't understand the argument against @paul-marechal 's suggestion. The chart from back in January is very straightforward. We already have public | protected | private {readonly} which covers 6 of the 9 cases. The other 3 cases are trivially solved with two modifiers ordered with broader access before narrower -- public protected, public private, protected private. If we assume that write implies read, and narrower access is never more restricted than a broader one, these sequences must mean "public read, protected write", "public read, private write", and "protected read, private write" respectively.

There are other issues to argue about a writeonly keyword; I don't think we're going to get it any time soon. Likewise, I don't think anybody has made a realistic argument in favor of public-write / protected-readonly. I see @trusktr took a stab at it, but he's actually talking about getters and setters -- providing the appearance of a read/write property through a public API. That's also a different issue (Ryan linked it near the top of the thread), this is specifically for "plain" property definitions.

trusktr commented 2 years ago

@ifeltsweet

All other variations like privateread would be quite unnecessary in my opinion and are solving for a completely different use case than what was originally requested by this issue.

It is not unnecessary, but actually useful. It's about semantics: I want to define something that I can read from, but I want only the outside to modify it. I want it to be explicitly an input only.

There are real world use cases I already pointed out, but here is another one: props in various frameworks like React, Vue, Solid, etc, are usually meant to be readonly on the inside, despite that some of these libs' runtimes allow modifying input props (and their documentation highly recommends not to modify props despite that you can, and some even spit warnings to console in dev mode).

Again, this isn't about technicality, this is about expression of intent.

Writable on the outside, readonly on the inside, is totally valid, and real-world practice. We simply need to make the language to describe actual real-world code.

thw0rted commented 2 years ago

@trusktr see my comment above about "providing the appearance of read/write". Public write / private read is a relatively rare use case -- important to describe a limited number of frameworks. It's not a design pattern for everyday use, not something to be encouraged, and should be implemented via property accessors anyway.

I would contend that it's more important to have a one-line shorthand for write-implies-read cases. Note that this was your original use-case in the OP, private _foo: string; public get foo(): string { return this._foo; } etc. Then, write-without-read cases could be described via variant accessors -- public set foo(x: string); private get foo(): string; -- mirroring the actual implementation.

In case they're not already linked from here, other issues to follow for differing accessor modifiers and writeonly: #21759, #30852, #43662 , #45517.