-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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: read-only modifier #12
Comments
I think this is probably the most important feature in terms of making Typescript a safer language. E.g. You are guaranteed that the object will not be mutated along the way and that simplifies reasoning a lot. however, I don't see the problem with magnitudeSquared, it does not change 'v', so, it should work fine with both read-only and mutable objects. |
👍 |
The problem is, you don't know this without reading the function implementation, and you might not have access to the implementation. Let's say someone gives you their library with a 1.0 TypeScript definition file: // Docs say: calculates the squared magnitude of a vector
declare function magnitudeSquared(v: { x: number; y: number }): number;
// Docs say: rescales a vector to have length 1 and returns
// the scale factor used to normalize the vector
declare function normalize(v: { x: number; y: number }): number; One of these functions modifies the object, one of them doesn't. You can't tell by looking at their signatures -- they're identical. Now we have two obvious options Assume all functions arguments are actually treated as read-only?That fails to catch this problem var p: ImmutablePoint = { x: 1, y: 3 };
var pN = normalize(p); // Failure to catch immutability violation Worse, now you have to opt in to non-readonliness. When you upgrade your definition file to have the read/write information, now you're going to have to write // I have to write the word 'mutable' this many times?
declare function normalize(v: { mutable x: number; mutable y: number }): number;
declare function invert(v: { mutable x: number; mutable y: number, mutable z: number }):void;
declare function changeRotation(v: { mutable theta: number; mutable phi: number }): void; Assume all function arguments are actually mutable?Now I can't use var p: ImmutablePoint = { x: 1, y: 3 };
var len = magnitudeSquared(p); // Error, this function might modify p! And you get to write // I have to write the word 'readonly' this many times?
declare function magnitudeSquared(v: { readonly x: number; readonly y: number }): number;
declare function length(v: { readonly x: number; readonly y: number, readonly z: number }): number;
declare function dotproduct(v: { readonly theta: number; readonly phi: number }): number; |
Can this be re-opened as a request? Given the increased use of functional programming with JavaScript and immutable data structures (see React and Flux). I think this would be great as an opt-in feature, maybe with a top-level decorator enabling it to avoid the issues of default behavior (i.e. ignore the check by default). |
I'm not sure what you're asking? The issue is currently open. |
Sorry, misread the status. |
+1 for readonly keyword support. |
+1 for something along these lines. The problems described by @RyanCavanaugh are quite real but the suggestion by @joewood to have some sort of decorator seems to be a smart one. I guess it might be along the lines of 'use strict' at the start of functions but probably more explicit, like Perhaps along the same lines, where ? is used on a parameter to indicate it's optional, could other annotations be added, along with one indicating either (like how |
"The perfect is the enemy of the good." As TypeScript stands today, the compiler won't even tell me when I set a property with no setter -- with the full implementation available. This has been raised as an issue multiple times, and those other issues all point back to this one, so it seems pretty important that progress be made in this area. To me, Edit: Removed reference to readonly decorator. Readonly is a tar pit, and my point is around get/set, not interfaces. |
+1 for a readonly modifier or similar. The scenario I am frequently running into is wanting to define an interface with properties that only requires a getter, which I would then expect the compiler to enforce by generating an error if the code tries to set a readonly property. Implementations would be free to provide a setter if they choose to do so, but it should only be usable in code if the object is being referenced as the implementing class or a derivative of it. My personal preference would be the way c# does it, allowing you to specify get and/or set in the interface, but the readonly case is by far the most useful (other than the full get/set case). |
Thinking out loud here: While Suppose you have:
Given the properties of an So that:
The
The compiler can then optimize the Some problems remain with this:
|
I was actually thinking of something more like the interface Foo {
x : number;
getConstBar() const : const Bar;
getBar() : Bar;
}
function doNotMutate( obj : const Foo ) {
obj.x = 3; // !! compiler error
var bar = obj.getBar(); // !! compiler error, cannot call a non-const function
var constBar = obj.getConstBar(); // OK, can invoke a const function now have a const Bar
}
function mutate( obj: Foo ) {
obj.x = 3; // works
var bar = obj.getBar(); // works too
} This way it's an opt-in on the interface, and can be passed down to child objects through accessor functions (or getters) that are declared So, in practical terms for libraries like react.js, the |
I prefer Eyas' suggestion. It's clean and simple. JavaScript has so many ways to modify state: adding/removing/deleting properties, side-effecting getters, proxies, etc. that introducing a concept like const will necessarily be horribly complicated. There's also the issue of claiming that a method or argument in a JS library is const when it turns out not to be; there's no way to check it and not even runtime errors will be generated unless the compiler wraps arguments in a read-only proxy. |
Perhaps even more explicit, another suggestion similar to my previous one: No clone & freeze, simply,
A var pt3: Point = { x: 1, y: 1 };
var pt4: ImmutablePoint = pt3; // NOT OK -- Point and ImmutablePoint are incompatible
var pt5: ImmutablePoint = pt3.toImmutable(); // OK, structurally compatible
pt3.x = 5; // pt4.x -- does not change The call The type definition for |
👍 I really like Eyas' suggestion, but the |
I've summarized some of my thoughts on immutables in TypeScript. May be this could be help. // Primitives
val x = 7, y = 'Hello World', z = true; // ok -> all primitives are immutable by default
// Objects
val p1 = { x: 120, y: 60 }; // ok -> all attributes are immutables
p1.x = 45; // error -> p1 is immutable
p1['y'] = 350; // error -> p1 is immutable
var p2 = { x: 46, y: 20 };
p1 = p2; // error -> p1 is immutable
val keyValue = { key: 'position', value: p2 }; // error -> p2 is not immutable
// Arrays
val numbers = [0, 1, 2, 4]; // ok -> all elements are immutable
numbers.push(9); // error -> numbers is immutable
val points = [p1, p2]; // error -> p2 is not immutable
// Functions
type Point2D = { x : number, y : number };
function multiply(s : number, val point : Point2D) {
return val { x: s * point.x, y: s * point.y };
}
multiply(2, { x: 32, y: 20 }); // ok -> { x: 32, y: 20 } is immutable
multiply(2, p1); // ok -> p1 is immutable
multiply(3, p2); // error -> p2 is not immutable
function multiply2(s : number, val point : Point2D) : val Point2D {
return { x: s * point.x, y: s * point.y };
}
// Type Assertion
val p3 = <val Point2D>p2;
// Classes
class Circle {
private x: number;
private y: number;
private val r: number;
constructor(x : number, y : number, r : number) {
this.x = x;
this.y = y;
this.r = r; // ok -> in constructor
}
setRadius(r: number) {
this.r = r; // error -> this.r is immutable
}
}
var circle = new Circle(100, 200, 20);
circle.x = 150; // ok -> circle.x is not immutable
class Circle2 {
constructor(private val x : number, private val y : number, private val r : number) { }
} |
I am very strongly in favor of a conservative approach here. Probably the simplest would be allowing interface Point {
const x: number;
const y: number;
}
var origin: Point = {
x: 0,
y: 0,
} The semantics of That is, the following is allowed: interface Foo {
const map: { [key: string]: string };
}
var x: Foo = { map: {} };
x.map['asdf'] = 'hjkl'; The compiler should assume all properties are mutable unless told otherwise. This sucks for people who like to use objects in a read-only fashion (myself included), but it tracks the underlying JavaScript better. I can think of at least four kinds of immutability in JS:
From a type perspective, numbers 2 and 3 act the same, but the machinery is technically different. I see some comments above about annotating an entire object as immutable, which wanders into the territory of point 4, but I think that opens a can of worms. (More below.) I thought about giving a rough spec, but here's a fun question: Given: interface Foo {
const x: number;
}
interface Bar {
x: number;
}
var x: Foo = { x: 1 },
y: Bar = { x: 1 }; Is the following assignment compatible? var z: Foo = y; That is, can we allow mutable properties to "downgrade" into immutable variations? It makes sense in function scenarios: function showPoint(point: { const x: number; const y: number }): string {
return "(" + point.x + ", " + point.y + ")";
}
console.log(showPoint(x), showPoint(y)); While (rather) confusing, it does have the advantage (?) of jiving with what JavaScript offers with get-only properties: var x = 0;
var obj = {
get x() {
return x++;
}
};
console.log(obj.x, obj.x); Despite the question above, I would suggest behavior like the following: the presence of interface Foo {
const x: number;
}
var x: Foo = { x: 1 }; // okay
var y: Foo = { get x(): { return 1; } }; // also okay, because 'x' is readable
var z: Foo = { set x(_x): { return; } }; // no, 'x' has no getter Compilation output for a class would be roughly as follows: class Foo {
const x = 1;
} var Foo = (function () {
function Foo() {
Object.defineProperty(this, "x", {
value: 1,
enumerable: true,
configurable: true,
writable: false,
});
}
return Foo;
})(); Not sure how it would compile for a module. Perhaps it would use Declaring a function as That is, given the following:
Any instance of This can cause issues with declaration merging, though. interface Foo {
const func(): void;
}
interface Foo {
func(x: number): void;
} In the case above, have we a) implicitly added a setter for the property My gut says we should go with interpretation b, given the intent we're trying to express. And if that's the case, then perhaps there should be interface syntax for getter and setter properties so that the following won't be an error: interface Foo {
get x(): number;
}
interface Foo {
set x(_x: number): void;
} As opposed to above, the intent here is not immutability but the allowed operations on a property. As an aside, I'm on the fence about allowing interface Whitelist<T> {
const [key: string]: T;
} Perhaps it's best to leave it off for now and let it come into existence if people ask for it. With all of this in hand, the notion of an immutable object seems easy: add some keyword (perhaps But then you have the unenviable position of figuring out if a method is safe to call on an immutable interface: immutable interface Vector {
x: number;
y: number;
magnitude(): number; // safe
normalize(): void; // not safe
} It's not really enough to look at var x = {
x: number;
y: number;
debug() {
console.debug("vector: ", this.x, ",", this.y);
}
}; It's clearly safe to call So to make a long reply short (too late), we could add |
There is also an EcmaScript 7 proposal on immutable data structures: https://github.com/sebmarkbage/ecmascript-immutable-data-structures |
To add to that, keep in mind that |
I'm not sure the immutable data structures proposal solves the problem of how to annotate that some properties can't be modified. They're two separate issues. (I'm not married to the |
I guess with first class immutable data structures the type system will need some way to signify immutability . This will allow the compiler to do compile time checks. |
excellent! |
Reset the noEmitForJsFiles option when updating compiler options (#12…
An important missing side note: #13003 (comment)
In plain English it means that #13002 is a use case yet to be looked at |
I didn't find anywhere best practices for readonly instead of get-only properties. |
@NN--- Generally the best practices are: private readonly members and public and protected get-only properties on classes. Use readonly for interfaces that will be implemented by classes that have get-only properties. Can add examples later. OTOH maybe StackOverflow, especially their new docs site is a better venue. |
@joelday TypeScript misses shorthand for such get-only properties. |
A getter only property will be inferred as readonly automatically. |
@mhegazy I understand this. interface A {
readonly x: number;
}
class B implements A {
readonly _x: number;
get x(): number {return this._x; }
}
class C implements A {
get x(): number; // This should be equivalent to B.x implementation
} |
And readonly x: number is not good enough? |
Or u r looking for public get private set? |
It is debatable and as I said I didn't find what is considered to be a good practice. |
Terse get syntax like C# was discussed in #10911. Since properties (at the moment) aren't ES spec, it could be argued though that the class A {
readonly a = 'foo';
get b() {
return 'bar';
}
} Would de-sugar to: var A = (function () {
function A() {
}
Object.defineProperty(A.prototype, "a", {
value: 'foo',
writable: false,
enumerable: true,
configurable: true
});
Object.defineProperty(A.prototype, "b", {
get: function () {
return 'bar';
},
enumerable: true,
configurable: true
});
return A;
}()); |
Why is readonly diffrent from private? Privacy is not maintained in the generated js either. |
@mhegazy You are correct. I mistakenly thought that private members are implemented in some different way. I guess it is offtopic now. It needs some other syntax for truly readonly properties to be implemented by defineProperty and privates via symbol or some other nice hack. |
The usage of This following code compiles, but is it semantically correct?
The member I would like to know if this use of |
@paleo readonly is similar to const, it doesn't define deep immutability, just some way to make less mistakes. const x = {a:1};
x = {a:2}; // Error , cannot change x
x.a = 2; // Ok, const is not deep let x: {readonly a:number;} = {a:1};
x = {a:2}; // x is mutable do whatever you want
x.a = 2; // Error, 'a' is readonly and doesn't allow reassignment. For true immutability, you need to write code which makes types immutable. |
@paleo |
that's not true either: #13002 |
@Aleksey-Bykov Previous issue title was a bit closer then... |
I quote @ahejlsberg from his PR:
In my question on StackOverflow, I wrote two use cases using Is that what the modifier |
Like a lot of the TypeScript typing system, it is not enforceable at run-time, it is a type contract. |
Thank you, it's clearer. I realize that I already had the answer in 2016, from @RyanCavanaugh . At that time the ES5 getter solution was not available to me because my code had to remain ES3 compatible, so I left angry and didn't take the time to understand the information. He said:
This is the final answer: yes, the |
Some properties in JavaScript are actually read-only, i.e. writes to them either fail silently or cause an exception. These should be modelable in TypeScript.
Previous attempts to design this have run into problems. A brief exploration:
Possible solutions?
The text was updated successfully, but these errors were encountered: