Readonly properties and index signatures by ahejlsberg · Pull Request #6532 · microsoft/TypeScript (original) (raw)
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 andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Conversation49 Commits34 Checks0 Files changed
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
[ Show hidden characters]({{ revealButtonHref }})
This PR adds support for readonly properties and index signatures as suggested in #12. Changes include:
- A new
readonly
modifier that can applied to property and index signature declarations. - A property or index signature declared with the
readonly
modifier is considered read-only. - Read-only properties may have initializers and may be assigned to in constructors within the same class declaration, but otherwise assignments to read-only properties are disallowed.
- Assignments to read-only index signatures are always disallowed.
Entities are implicitly read-only in several situations:
- A property declared with a
get
accessor and noset
accessor is considered read-only. - In the type of an enum object, enum members are considered read-only properties.
- In the type of a module object, exported
const
variables are considered read-only properties. - An entity declared in an
import
statement is considered read-only. - An entity accessed through an ES2015 namespace import is considered read-only (e.g.
foo.x
is read-only whenfoo
is declared asimport * as foo from "foo"
).
Some examples:
interface Point { readonly x: number; readonly y: number; }
var p1: Point = { x: 10, y: 20 }; p1.x = 5; // Error, p1.x is read-only
var p2 = { x: 1, y: 1 }; var p3: Point = p2; // Ok, read-only alias for p2 p3.x = 5; // Error, p3.x is read-only p2.x = 5; // Ok, but also changes p3.x because of aliasing
class Foo { readonly a = 1; readonly b: string; constructor() { this.b = "hello"; // Assignment permitted in constructor } }
// Read-only view of Array with all mutating methods removed interface ReadonlyArray { readonly [x: number]: T; readonly length: number; toString(): string; toLocaleString(): string; concat<U extends ReadonlyArray>(...items: U[]): T[]; concat(...items: T[]): T[]; join(separator?: string): string; slice(start?: number, end?: number): T[]; indexOf(searchElement: T, fromIndex?: number): number; lastIndexOf(searchElement: T, fromIndex?: number): number; every(callbackfn: (value: T, index: number) => boolean, thisArg?: any): boolean; some(callbackfn: (value: T, index: number) => boolean, thisArg?: any): boolean; forEach(callbackfn: (value: T, index: number) => void, thisArg?: any): void; map(callbackfn: (value: T, index: number) => U, thisArg?: any): U[]; filter(callbackfn: (value: T, index: number) => boolean, thisArg?: any): T[]; reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number) => T, initialValue?: T): T; reduce(callbackfn: (previousValue: U, currentValue: T, currentIndex: number) => U, initialValue: U): U; reduceRight(callbackfn: (previousValue: T, currentValue: T, currentIndex: number) => T, initialValue?: T): T; reduceRight(callbackfn: (previousValue: U, currentValue: T, currentIndex: number) => U, initialValue: U): U; }
let a: Array = [0, 1, 2, 3, 4]; let b: ReadonlyArray = a; b[5] = 5; // Error, elements are read-only b.push(5); // Error, no push method (because it mutates array) b.length = 3; // Error, length is read-only a = b; // Error, mutating methods are missing
Note that the readonly
modifier does not affect subtype and assignability relationships of the containing type for the reasons described here. Also note that this PR specifically doesn't attempt to formalize immutable objects. However, with the readonly
modifier it becomes a lot more convenient to implement such objects.
The PR still needs tests, but I thought I'd get it up there for folks to take a look at.
sergey-shandar, trunda, cluda, garkin, vvakame, RyanQuackenbush, agu-z, NoelAbrahams, iam3yal, gulshan, and 18 more reacted with thumbs up emoji sergey-shandar, mquandalle, SlurpTheo, sccolbert, monagames, michaelmesser, guncha, ladas-larry, pwnall, Meowtimer, and 4 more reacted with hooray emoji sergey-shandar, mquandalle, cluda, ulrichb, michalstocki, iam3yal, pgatilov, ryanmillerdev, mohsen1, jonaskello, and 4 more reacted with heart emoji
Conflicts: src/compiler/parser.ts src/compiler/types.ts
@@ -134,6 +134,8 @@ namespace ts { |
---|
const anySignature = createSignature(undefined, undefined, emptyArray, anyType, 0, /*hasRestParameter*/ false, /*hasStringLiterals*/ false); |
const unknownSignature = createSignature(undefined, undefined, emptyArray, unknownType, 0, /*hasRestParameter*/ false, /*hasStringLiterals*/ false); |
const enumNumberIndexInfo = createIndexInfo(stringType, /*isReadonly*/ false); |
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this indexer not readonly?
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe because we don't want to break this code:
enum SyntaxKind { Node, OtherNode } function dbgPrint(value: number, yourEnum: { [n: number]: string }) { console.log(yourEnum[value]); } // Would-be error, dbgPrint wants mutable index signature but enum has readonly signature dbgPrint(SyntaxKind.OtherNode, SyntaxKind);
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah right, because of the assignability rule. Makes sense.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's right. It's bit of a puzzle to ensure backwards compatibility, so there are actually two forms of read-only-ness in the new implementation:
readonly
properties,readonly
index signatures, andget
only accessors are always considered read-only, in both assignments and type compatibility checks.const
variables, functions, classes, namespaces, enums, and enum members are considered read-only in assignments, but not in type compatibility checks. This ensures backwards compatibility. For example, it is reasonably common to implement an interface contract using a module or namespace, and strict read-only-ness would break that pattern.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. That sounds like a good distinction.
// -- operator on enum type |
---|
enum ENUM1 { A, B, "" }; |
// expression |
var ResultIsNumber1 = --ENUM1["A"]; |
~~~~~~~~~~ |
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As clarification, increment/decrement operator and any assignment to enum isn't allowed as enum type is read-only upon assignment? if that is the case, I think the error message should be updated.
Just stumbled upon this. Don't think I could be any happier than I am right now.
@ahejlsberg These questions in #8496 (and the issue in general) are highly relevant to the topic here. Consider providing your comments. I have only closed that issue because I felt that your (unbelievably stubborn and righteous) team members would somehow find it more 'respectful' that I show my complete pessimism at having any influence here than trying to 'confront' them in any way. You have already won the 'battle', congratulations, now feel free to join the discussion.
Unbelievably stubborn and righteous, eh?
[I apologize for this being very off-topic but since I was asked I felt I had no choice but answering here, the person who asked doesn't have a public email so there is no way for me to contact them personally]
Yes, my personal impression is that they are very 'difficult' people to deal with, in the sense that they are putting as much energy into 'justifying' (sometimes dishonestly or irrationally) their actions and decisions to actually considering things openly and objectively when presented to them. They even do this on some very trivial subjects (e.g. you tell them they don't conform to the ES6 spec by practically disallowing inheritance of static accessors for ES5 targets and instead of listening and acknowledging the problem like a normal person would do, a group of team members just 'jumps' on you and starts to throw some strange and arbitrary ('canonical') justifications about performance consideration and dubious statistics there is '0%' anyone would ever want to do this.. well I did! and so did others who reported this problem, and probably many who didn't.. :( ).
They put you in a situation where you have to be so eloquent and rational, to the point you have to literally spend dozens of hours into very carefully and rationally crafting a 'thesis' for something otherwise relatively trivial, just for them. And they on the other side are allowed to simply 'push' their opinions and twisted patterns of 'justification' arbitrarily without anyone saying anything. When you start challenging those patterns they would either ignore you or tell you you are intentionally 'wasting their time' with things that aren't important. They would also use their 'internal' understanding of the language and compiler by mentioning intricate things or concepts you don't know or completely understand and dishonestly use that against you.
They take submitted ideas they like but they don't actually partake in real two-way conversations about it. They don't really involve the submitter of their personal ideas and insights, at least before they decide on their actions. And when/if they do inform back, they would condense it to a set of a few sentences that are not very useful or enlightening by themselves (or direct you to a set of design 'notes' that are very hard to extract anything useful from). You can't ask a (non-trivial) question because that's just 'wasting their time' (they don't do 'one on one').
If they don't like an idea, they would usually 'tank' and attack it mercilessly to the point of almost (or perhaps actually) insulting the person who submitted it. They almost never 'improve' on things suggested to them, at least not publicly. They would rather spend their time and 'smarts' in crafting careful arguments for justifying why they think someone are 'wrong' and they must be 'right' than taking a step back to seriously and objectively reconsider things, and eventually thank the participant for contributing something useful to the discussion.
This has been my experience so far, anyway. I'm mostly referring here to the 2-3 members who reply to GitHub issues.
Is there any reason / chance to change that the generated code contains regular property assignment and not defineProperty
with writable: false
? That would help a lot to protect against runtime assignment to properties designed as readonly
.
I agree with @RReverser - on my team we frequently use TypeScript compiled code from (legacy)JavaScript applications - so keeping as many of those protections as possible would be very helpful.
Is this already available or only for TypeScript 2.0 (neither VS nor TypeScript Playground seem to understand it)
@Aides359 As usual with all the fresh stuff, you need to use npm i typescript@next
to get the latest compiler with experimental features included.
@Aides359 I think the playground hosts the latest stable release. I recommend doing what @RReverser suggests and additionally installing vscode and setting then "typescript.tsdk"
property of your user/workspace settings to the install location's lib directory.
Read-only properties may have initializers and may be assigned to in constructors within the same class declaration, but otherwise assignments to read-only properties are disallowed.
Why disallow assignments in the other methods of a class?
A readonly
property would be useful to implement a getter with a true property (better for performance, readability):
class Foo {
readonly a = 1;
inc() {
++this.a; // Why not allow to do this?
}
}
Does the limitation exist for a technical reason?
If that were allowed we'd have daily reports about "Bug: Why does TS let me mutate readonly
fields?" 😉
We discussed the notion of "public readonly, protected mutable" but opted not to simply due to complexity. You can already have a getter-only wrapper around a private variable, which achieves the same semantics at the cost of slightly increased class size.
OK. You use the term readonly
as a synonym for const
. I hoped an accessor readonly
. :(
I use this great feature for mutable preventing by the syntax highlighting. Thanks a lot for this!
+++
This change isn't as bad as it might sound because we'll always error on direct assignments to readonly properties and that's where the majority of the errors occur.
this is a horrible change, rendering this feature usless
simple use case that fails, I have my app about 1500 TS files thick, in this app I have an interface named Data that was intended to be immutable historically could not be forced being so
With the readonly feature announced it was immediately clear that such enforcment can be imposed.
However it turned out that there were a few places where Data was meant to be mutable.
To address it the original Data interface was split into ReadOnlyData and WitableData, however it didn't help much due to the said decision. Details can bee seen in #13002.
Bottom line is we are still not sure if Data is effectively mutable or not, despite applibg readonly
to all its properties.
@ahejlsberg Correct me if I'm wrong, but the only reason interface compatibility checking on readonly is a breaking change is because of readonly being implicitly applied to everything. The tradeoff of worse soundness in return for saving the user some typing, especially when you now have mapped types, is not a good one IMO.
microsoft locked and limited conversation to collaborators