-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
[feature] class properties that are "readonly in public, writable in private" or other permutations #37487
Comments
Previously at #2845 but we should revisit after so long |
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 |
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 // 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 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 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 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. |
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 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.
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;
}
// ...
} |
This is one of the features I miss so much in TypeScript. I don't really like the 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 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. |
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 @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. |
The
|
@whzx5byb I appreciate your example, but it is not a solution to the problem.
|
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. edit: Actually you would never need to mix the I don't really see a case where someone would want it the other way around, like "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. |
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 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. |
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 // Publicly read-only, otherwise protected.
public readonly protected foo: Bar;
// Protected read-only, otherwise private.
protected readonly private foo: Bar; |
👍 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
Reasons I like the swift style:
The downsides to using the swift style is that |
Nonetheless, it may not be ideal. Just my personal preference. There are plenty of other options. |
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. |
If you perform a cast, then the value can be changed (playground), although it looks a little strange. |
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
|
@mixtur So would your proposition look like the following? class Foo {
public(get) protected name: string;
} |
@marechal-p
No. Just class Foo {
public(get) name: string;
} This would mean that the only forbidden thing is public write. Ow. I see your point. |
@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 |
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. So according to this I suggest case 1 - cases 4 and 5 I think are rare so it is fine for them to require more noisy syntax. case 4 - |
@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: My syntax suggestion for the additional cases would be: Where Case 1 and 4 could be further simplified if |
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. |
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. |
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. |
I don't understand the argument against @paul-marechal 's suggestion. The chart from back in January is very straightforward. We already have There are other issues to argue about a |
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. |
@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, In case they're not already linked from here, other issues to follow for differing accessor modifiers and |
Please no keywords like |
I agree that the compound words are ugly but TS is based on JS / ES, not Swift, and has no other cases where a modifier uses function-like syntax similar to what you've linked. I believe that there are combinations of existing reserved words, in an order that is currently a syntax-error (and thus shouldn't be a breaking change!), which would suffice to implement this feature. |
After going through the These write modifiers play well with the standard access modifiers class Order {
// Can write from this class only, read from anywhere
privatewrite price: number;
// Same as above, but explicit public access
public privatewrite price: number;
// Accessable from this class and subclasses, writable only from this class
protected privatewrite price: number;
// Compiler/linter warning because privatewrite has no effect
private privatewrite price: number;
// Compiler error because readonly conflicts with privatewrite
readonly privatewrite price: number;
} |
Feel like this suggestion is blocked because of we haven't found a syntax people love, so throwing another option into the ring: OverloadingUndo the requirement that members must have identical modifiers, and embrace the modifiers being different: class Dog {
public readonly name: string;
private name: string = "abc";
}
class Cat {
protected readonly _age: number;
private _age: number = 123;
} The separate modifier overloads are collapsed together and tested against based on where the access is happening. Only one override can set a default value. Remove the duplicate identifier warning, but add a warning if overloads are not adjacent (same as method overloads). |
I like this idea because it reinforces the notion that access modifiers, like function parameter types, are a type-space construct. I'm not clear on your Cat example though - what does it mean to have two |
That would be like my suggestion at #43553 which also has the benefit of supporting stricter types for broader access scopes. |
Similar thoughts on this one. Regarding the Suggestion for compile-time error: I think, as a rule of thumb, the |
Thanks! That was a typo. Second one should've been |
I think the best way to implement this is a possibility to add different types for each access modifiers.
|
If property has an initializer, where should I pass it in your example?
|
I think it shouldn't be matter, just use one of them. |
Hi. There is a tc39/proposal-grouped-and-auto-accessors that suggested syntax for read-only field, but using same syntax as ECMAScript private fields. This will be compatible with ECMAScript decorators currently being worked on for TS 5.0 #48885 // code from proposal
class C {
accessor a = 1; // same as `accessor a { get; set; } = 1;`
accessor b { } = 1; // same as `accessor b { get; set; } = 1;`
accessor c { get; set; } = 1; // same as `accessor c = 1;`
accessor d { get; } = 1; // getter but no setter
accessor e { set; } = 1; // setter but no getter (use case: decorators)
accessor f { get; #set; }; // getter with private setter `#f`;
accessor g { #set; } = 1; // private setter but no getter (use case: decorators)
accessor #h = 1; // same as `accessor #h { get; set; } = 1;`
accessor #i { } = 1; // same as `accessor #i { get; set; } = 1;`
accessor #j { get; set; } = 1; // same as `accessor #j = 1;`
accessor #k { get; } = 1; // getter but no setter
accessor #l { set; } = 1; // setter but no getter (use case: decorators)
} So, I suggest just add access modifiers into brackets like this: accessor a { get; protected set; };
accessor b { get; private set; };
accessor c {
get() { ... }
protected set(value) { ... }
}; |
That looks great as far as accessor properties are concerned but this thread is largely concerned with data properties. |
@sviat9440 There are two options, it could work exactly as overloads on functions do right now, so you'd declare the implementation last (and independently) like this: public readonly foo: ReadonlyMap<string, string>;
private foo: Map<string, string>;
foo = new Map<string, string>(); Second option is to combine an overload signature and an implementation. This would be slightly different to how overload functions work now (but slightly more convenient), because if an overload signature exists the base signature of the function is not used outside the implementation. public readonly foo: ReadonlyMap<string, string>;
private foo: Map<string, string> = new Map(); The implementation should be last for style reasons, but it shouldn't be enforced (otherwise ESLint would probably generate an All signatures should be adjacent or TS generates a Signatures must be compatible with the implementation or TS generates a Having a second implementation would make TS generate a |
@dhoulb go idea I think force add declare protected foo :number;
private foo :number = 1; At least forbid to write assignment not at last to avoid: private foo :number = 1;
protected readonly foo :number; being transformed by old type eraser util to: foo = 1;
foo; which cause runtime |
I like this as it's common to expose a sub-class publicly. class Example {
public readonly data: ReadonlyMap<string, string>
private readonly data: Map<string, string>
// ...
} Also lets us hide function parameters: class Example {
public readonly doStuff()
private readonly doStuff(hiddenOption)
doStuff(hiddenOption) {
// do stuff
}
} This also has the benefit of not introducing new keywords. |
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
we would be able to write something shorter like
Checklist
My suggestion meets these guidelines:
The text was updated successfully, but these errors were encountered: