Closed RyanCavanaugh closed 8 years ago
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.
:+1:
I don't see the problem with magnitudeSquared, it does not change 'v',
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
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 mutable
basically everywhere:
// 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;
Now I can't use magnitudeSquared
without a cast:
var p: ImmutablePoint = { x: 1, y: 3 };
var len = magnitudeSquared(p); // Error, this function might modify p!
And you get to write readonly
everywhere:
// 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
mutable class
vs immutable class
and
mutable module
vs immutable module
to specify the defaults within a block of code at the module or class level. For back-compat reasons, mutability would have to be assumed, right?
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 any
throws away types)?
"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, having the readonly decorator and catching the assignment-with-no-setter error would be a huge step in the right direction, even if it doesn't solve the third-party library problem.
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 readonly
fields are important, perhaps an easier thing to implement, which doesn't have some of the problems @RyanCavanaugh outlined is immutable
classes/interfaces.
Suppose you have:
immutable interface ImmutablePoint {
x: number;
y: number;
}
Given the properties of an imutable
object, we can change how assignment behaves, to instead clone and freeze in JS.
So that:
var pt3: Point = { x: 1, y: 1 };
var pt4: ImmutablePoint = pt3; // OK -- pt3 is copied
pt3.x = 5; // pt4.x -- does not change
The pt4
assignment would basically look like:
...
// compiled JavaScript
var pt4 = pt3.clone(); // your favorite clone() scheme
pt4.freeze();
...
The compiler can then optimize the clone & freeze
into only a freeze
if:
1- we are using a literal assignment
2- possibly, if the variable/value we are copying is dead after this line (when dealing with global scope ,etc.)
Some problems remain with this:
magnitudeSquared
problem, we'd need to declare it as magnitudeSquared(v: immutable v { x: number, y: number })
.I was actually thinking of something more like the const
modifier in C++. This would be applied to the object instance, providing an immutable view of an existing object. So something more like:
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 const
. Declaring a function const
essentially applies the modifier to this
(again, in the same way as C++ works).
So, in practical terms for libraries like react.js, the setState
function would be mutable but the accessor functions to state
and props
would be immutable.
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, immutable {...}
can only be assigned by types that are:
A toImmutable()
on every object
converting object
to immutable object
is created. Which basically has clone and freeze symantics. Thus:
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 .toImmutable();
basically implements clone and freeze.
The type definition for freeze()
could also be augmented.. not sure how yet.
:+1: I really like Eyas' suggestion, but the .toImmutable();
function should not be part of TypeScript.
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 const
for properties in declarations:
interface Point {
const x: number;
const y: number;
}
var origin: Point = {
x: 0,
y: 0,
}
The semantics of const
here would be the same as if it were a binding: no assignments are allowed. Nothing else would be implied (the C++ const semantics would get hairy pretty quickly).
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:
const
bindings, as offered by ES6,writable: false
in their descriptors, andObject.freeze
.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 const
in a property declaration is treated by the compiler as a read-only value; any and all assignments are banned. In the presence of const properties, assignments are compatible if the compiler can verify that the property is readable:
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 Object.defineProperty
on the module object or instead try to align with ES6?
Declaring a function as const
would have the possibly counterintuitive effect of making the function declaration immutable, not its return value.
That is, given the following:
interface Math {
const sin(x: number): number;
}
Any instance of Math
can't have its sine implementation replaced. (But users could say (<any> Math).sin = /* ... */
if they wanted.)
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 func
, or b) removed immutability? If the meaning is a, then we're okay. If it's b, then we should error, because the second interface has discarded a previously-existing guarantee.
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 const
on indexers:
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 frozen
to match the underlying Object.freeze
, or immutable
for obvious reasons) that means "all properties on this object are const
.
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 void
return types, though:
var x = {
x: number;
y: number;
debug() {
console.debug("vector: ", this.x, ",", this.y);
}
};
It's clearly safe to call x.debug()
even if it were of an immutable type because no property is modified. But I shudder to think of what its return type should look like. const void
?
So to make a long reply short (too late), we could add const
as a modifier to property declarations to indicate that the compiler should prevent the user from mutating them. It's difficult to make other guarantees.
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 const
exists in ES6. We should try to avoid overloading keywords that are going to have a specific meaning in future ES standards.
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 const
usage I invented. It was mostly to get the highlighter to work.)
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.
I think that the readonly keyword might be useful for private and static functions to allow for better minification of generated files. For example, I have AMD code like the following:
export class MyClass{
public myFunction() : number {
return privateFunc(3);
}
private readonly privateFunc(toSquare: number): number{
return toSquare * toSquare;
}
}
It would be more efficient minification-wise if the code were treated as follows and then generated:
function privateFunc(toSquare: number): number {
return toSquare * toSquare;
}
export class MyClass{
public myFunction() : number {
return privateFunc(3);
}
}
In the first block, "privateFunc" cannot be minified down to "a" while the second block it can. In which case the readonly keyword would be used to let the compiler know to make sure that my private/static function calls are only to other private/static functions and not to instance variables and that nowhere in the class is there code which does this.privateFunc = newFunction
I currently write code according to the second block, but only when I the private/static function follows the constraints listed in the prior paragraph. I would much rather have the compiler take care of that refactoring for me.
Thoughts?
I've tried to summarize some of the thoughts from the above thread. I am quite new to TypeScript (and JavaScript as well) - so, apologies if some of the suggestions are infeasible for one reason or another - however, I think that, at least from the specs perspective, the below could work:
interface Point {
x: number,
y: number
}
// declaring readonly properties is done with #
interface ImmutablePoint {
#x: number,
#y: number
}
var p1: Point = { x: 1, y: 1 }; // p1 is fully mutable
var p2: ImmutablePoint = { x: 2, y: 2 }; // underlying object is mutable
var p3: ImmutablePoint = #{ x: 3, y: 3 }; // underling object is not mutable
To illustrate the differences between the above:
p1.x = 100; // OK
p2.x = 100; // compiler error
p3.x = 100; // compiler error
var p4: ImmutablePoint = p1; // mutable objects can be cast down into immutable objects
p4.x = 100; // compiler error
p1.x = 100; // however, underlying object remains mutable and now p4.x is = 100
var p5: ImmutablePoint = #p1; // this will clone and freeze p1
p5.x = 100; // compile error
p1.x = 200; // does not affect p5.x
var p6: Point = p2; // compiler error
var p6: Point = <Point> p2; // immutable objects can be explicitly cast into mutable objects
p6.x = 100; // OK - object underlying p2 is mutable
// but
var p7: Point = <Point> p3; // OK - no way to check that object underlying p3 is not mutable
p7.x = 100; // runtime error - object underlying p3 is not mutable
// also
var p8: Point = #{ x: 1, y: 1 }; // compiler error
By default, function arguments are assumed to be mutable
function foo(v: { x: number; y: number }) {
v.x = 100;
return v.x * v.y;
}
foo(p1); // OK
foo(p2); // compiler error
foo(<Point> p2); // OK
foo(p3); // compiler error
foo(<Point> p3); // runtime error
but we can also explicitly make them immutable:
function foo(v: { #x: number; #y: number }) {
v.x = 100; // compiler error
return v.x * v.y;
}
foo(p1); // OK
foo(p2); // OK
foo(p3); // OK
There's a problem with allowing mutable objects to be accepted when a function expects an immutable object. Here is a slightly convoluted but illustrative:
var p1: Point = { x: 1, y: 1 }; // p1 is fully mutable
function foo(v: {#x: number; #y: number}): (factor:number)=>number {
return (factor:number) => (v.x+v.y) * factor;
}
var bar: (f:number)=>number = foo(p1);
var v1 = bar(1); // (1+1)*1=2
p2.x = 2; // legal because p1 is immutable
var v2 = bar(1); // (2+1)*1=3
// v1 !== v2
I think this is a strange behavior. foo
was written with the explicit intention of having the variable v
, which backs bar
(thanks to closures) be immutable.
I can see how this behavior is undesirable. I think there are 2 potentially conflicting use cases for read-only properties here.
Use case 1: underlying data for a read-only property should never change (your example above) Use case 2: underlying data for a read-only property could change, but not through the exposed interface. To illustrate
interface IPerson {
firstName: string,
lastName: string,
#fullName: string // this property is read only
}
class Person implements IPerson {
constructor (public firstName: string, public lastName: string) { }
get fullName(): string {
return this.firstName + ' ' + this.lastName;
}
}
var person = new Person('a', 'b'); // person.fullName is now 'a b'
person.firstName = 'c'; // OK - firstName is mutable and person.fullName is now 'c b'
person.fullName = 'x y'; // compiler error
Naturally, there are 3 options to handle this:
(1) sacrifice use case 1 in favor of use case 2 (2) sacrifice use case 2 in favor of use case 1 (3) somehow accommodate both use cases
To take a stab at the 3rd option: what if we have 2 different ways to define immutable parameters in function signature? specifically:
function foo(p: IPerson) {...}
function bar(p: #IPerson) {...}
var person = new Person('a', 'b');
foo(person); // OK - foo is able to modify mutable properties of person
bar(person); // OK - bar gets a cloned&frozen copy of person - no properties can be modified
After thinking about this some more, it seems that the distinction between "read-only interface" and "read-only data" may be 2 different issues and could/should be addressed separately.
This is when the data cannot be modified through the exposed interface, but may or may not be modifiable through other interfaces. To illustrate:
interface Person {
firstName: string;
lastName: string;
#fullName: string;
}
interface ImmutablePerson {
#firstName: string;
#lastName: string;
#fullName: string;
}
class Person1 implements Person {
constructor (public firstName: string, public lastName: string) { }
get fullName(): string {
return this.firstName + ' ' + this.lastName;
}
}
class Person2 implements Person {
constructor (public firstName: string, public lastName: string) { }
get fullName(): string {
return this.firstName + ' ' + this.lastName;
}
set fullName(value: string) {
// parse value and set this.firstName and this.lastName
}
}
// the above should result in the following behavior
var p1 = new Person1('a', 'b');
var p2 = new Person2('x', 'y');
p1.firstName = 'q'; // OK - and now p1.fullName is = 'q b'
p2.firstName = 'y'; // OK - and now p2.fullName is = 'y z'
p1.fullName = 'c d'; // compiler error
p2.fullName = 'x z'; // OK - and now p2.firstName is = 'x' and p2.lastName is = 'z'
var p3: Person = p2; // OK - implicit cast of mutable to immutable
p3.fullName = 'c d'; // compiler error - Person.fullName is read-only
p2.fullName = 'i k'; // OK - and now p3.fullName returns 'i k';
var p4: ImmutablePerson = p1; // OK - implicit cast of mutable to immutable
p4.firstName = 'n'; // compiler error - ImmutablePerson.firstName is read-only
in functions
function foo1(p: Person) { ... }
function foo2(p: ImmutablePerson) { ... }
function foo3(p: { firstName: string, lastName: string, fullName: string } ) { ... }
var p1 = new Person1('a', 'b');
var p2 = new Person2('x', 'y');
var p3: ImmutablePerson = p1;
foo1(p1); // OK
foo2(p1); // OK - implicit cast from mutable to immutable
foo3(p1); // compiler error - Person1.fullName is read-only
foo1(p2); // OK
foo2(p2); // OK - implicit cast from mutable to immutable
foo3(p2); // OK
foo1(p3); // compiler error - ImmutablePerson.firstName is read-only
foo2(p3); // OK
foo3(p3); // compiler error - ImmutablePerson.firstName is read-only
// but
foo1(<Person> p3); // OK - explicit cast from immutable to mutable
I think the above behavior could be achieved with a few relatively simple rules:
(1) If a property in an interface is declared as read-only: attempts to assign value to this property via this interface should result in compile errors (2) A read/write property can be implicitly cast into a read-only property (3) A read-only property can be explicitly cast into a read/write property
I think this is conceptually different from the above - and it relies more on what happens at runtime - rather than at compile time. Basically, the data cannot be modified through any interface. The approach could be as follows:
interface Person {
firstName: string;
lastName: string;
#fullName: string;
}
interface ImmutablePerson {
#firstName: string;
#lastName: string;
#fullName: string;
}
class Person1 implements Person {
constructor (public firstName: string, public lastName: string) { }
get fullName(): string {
return this.firstName + ' ' + this.lastName;
}
}
var p1: Person = new Person1('a', 'b');
var p2: #ImmutablePerson = p1; // p1 is cloned and frozen and the resulting object is assigned to p2
Basically, putting # in front of a type indicates that an object should be clones and frozen on assignment. The above could be also done as:
var p2 = #p1; // p1 is clones and frozen, and type of p2 now is equivalent to ImmutablePerson
This could also work in function signatures
function foo(p: #{firstName: string, lastName: string, fullName: string }) {
p.firstName = 'a'; // compiler error
}
var p1: Person = new Person1('a', 'b');
foo(p1); // OK - foo now has a frozen copy of p1 and altering p1 does not have any effect on argument p inside foo
Readonly should be implemented more gradually, in parts where it's known to not cause problems such as:
function something(readonly myvalue) {
myvalue.something = 5; // Compiler error
}
It would still be useful. In my current case I would like to define it in the interface method param.
interface Some {
something(readonly myvalue);
}
To make sure the implementing classes does not modify the value when called with something.
P.S. There is something peculiar with readonly parameters, cause it's easier to think that they always are readonly and only intention to mutate parameters should be written. E.g. function something(mutable myvalue)
, but this would require to add readonly implicitely to all parameters and explicitely to define intent to mutate parameters.
:+1:
Can we(actually you :D) just add the getter only property in interface first? I really want it, very badly! For the compatibility issue, I agree with the idea of "use strict" thing. And maybe a writable keyword for calling legacy code from strict mode.
strict {
var pt:ImmutablePoint = { x: 100, y: 200};
legacyFunction(writable pt);
}
Bump...this would become even more useful if (and maybe when) non-numeric enums become part of TS. The typical use case for enums involve zero state, just data.
:+1:
+1
+1
I would already be very happy if a simple compiler warning could be generated when trying to assign something to a property without a setter. I was refactoring by encapsulating fields into properties with only getters and forgot to change some references. (In hindsight I should have done a refactor -> rename and add an underscore to the field before changing it to private and exposing the getter). Silently failing is pretty bad if it's easy to do unintentionally :/
I agree with @drake7707, this was my main reason to start following this thread.
:+1: for @drake7707's idea; also the reason I am following this thread.
@drake7707 Here's the test from my quick shot at implementing this feature as I see it: https://github.com/joelday/TypeScript/blob/getSetKeywordAssignment/tests/cases/compiler/assignmentToReadOnlyMembers.ts
Basically, if you put "get" before an interface member or if a class "get" accessor property doesn't have a matching "set" accessor, assignment is not allowed. It probably has some holes and I could see "readonly" being a better keyword. I would very much appreciate seeing this polished up and included in the language.
In any case, as for the long history of this issue, (it's 12 out of 692) this isn't about the deep implications of immutability language features, it's about an extremely simple guard against assignment, either deliberately or when we can trivially detect, 100% for sure that the assignment will fail at runtime.
Looks good to me, the only thing that I could think of is the following:
class A {
protected _value:string = "initial";
get value():string { return this._value; }
}
class B extends A {
set value(val:string) { this._value = val; }
}
var b = new B();
b.value = "test";
alert(b.value);
I don't even know if this should be allowed, in any case, b.value is undefined
I commented in this issue because lots of issues got closed that pointed this issue out referring to this issue as root cause.
I'd like to second what was said above, readonly is totally not about immutability - you can have a readonly property without immutability, it only means you're not allowed to write to it, not that its value can't change. It's neither about complex typesystem features - the only thing that would really matter to our team would be a readonly keyword that emits errors on obvious attempts to write to a readonly property. For us it's ok if casting or otherwise losing type information breaks these guarantees, whatever, we still have implicit anys all over the place.
@drake7707 That's a perfectly good case. I don't think my class member scan is looking at inherited tree, so I should fix that. I think it would probably be useful to add support for the "set" keyword on interfaces to support the scenario where an extending interface wants to add writability. It probably makes sense to have an error when a set-only member is being read from, but nobody really uses write-only properties as far as I've ever seen.
@hdachev Thanks! The problem I see with how issues have been duped and closed to this issue is that simple assignment guarding has been conflated with the entire question of what immutability features should/shouldn't exist. Maybe there's a clever approach that satisfies this scenario in a better way, but in any case, I think it's reasonable to assume that most developers want the equivalent of "string Foo { get; }" in C#, despite the possibility that people focused on language design for this project might have an aversion to emulating a C# feature just because people want it.
This appears to be aligned with ES6 "const" as I understand it, but for members rather than variables. I do want to make it clear that I don't think get-only members should const implicitly when assigned to a variable. If people want more immutability than this, they can take the responsibility to do it through the deliberate use of this feature on everything that should be immutable rather than adding magic that complicates the language.
TL;DR This should work like it does in C# because it's simple and it's what people have been asking for. Also, it only took a day to prototype it.
(Edit: const implies that the value itself is never a different value or object reference, though, which is not the case here, so I don't think that is a good keyword to use here. I'm leaning towards keeping "get" because then it makes sense for adding a "set" version to an extended interface.)
@joelday I love where you're going with this.
I agree that it's just a matter of only being able to read a value. Getters without setters are readonly in JavaScript, generating a runtime error on violation. Constants generate the same error. They both could be better typed with read only support.
And many JavaScript libraries use this as well. It would benefit definition writers as well.
On Mon, Aug 24, 2015, 16:57 Joel Day notifications@github.com wrote:
@drake7707 https://github.com/drake7707 That's a perfectly good case. I don't think my class member scan is looking at inherited tree, so I should fix that. I think it would probably be useful to add support for the "set" keyword on interfaces to support the scenario where an extending interface wants to add writability. It probably makes sense to have an error when a set-only member is being read from, but nobody really uses write-only properties as far as I've ever seen.
@hdachev https://github.com/hdachev Thanks! The problem I see with how issues have been duped and closed to this issue is that simple assignment guarding has been conflated with the entire question of what immutability features should/shouldn't exist. Maybe there's a clever approach that satisfies this scenario in a better way, but in any case, I think it's reasonable to assume that most developers want the equivalent of "string Foo { get; }" in C#, despite the possibility that people focused on language design for this project might have an aversion to emulate a C# feature just because people want it.
This appears to be aligned with ES6 "const" as I understand it, but for members rather than variables. I do want to make it clear that I don't think get-only members should const implicitly when assigned to a variable. If people want more immutability than that, they can take the responsibility to do it through the deliberate use of this feature on everything that should be immutable.
TL;DR This should work like it does in C# because it's simple and it's what people have been asking for. Also, it only took a day to prototype it.
— Reply to this email directly or view it on GitHub https://github.com/Microsoft/TypeScript/issues/12#issuecomment-134377631 .
@RandScullard & @impinball Glad we're on the same page. :D
Providing only getter does not guarantee the immutability of the property.
class A {
private _value: string = 'initial';
get value(): string {
return this._value;
}
doSomething(): void {
this._value = 'another value'; // internal reassignment cannot be prevented.
}
}
Although this issue is mainly about externally declaring object properties as immutable, I'm concerned with the immutability of the internal state (maybe I should create another issue).
I hope some keyword (such as final
) could inhibit reassignment of instant variables as follows:
class A {
private final str = 'str';
private final num: number;
constructor(private final bool: boolean) {
this.num = 0;
}
doSomething() {
// The below codes raise compilation errors
this.str = '';
this.num = 0;
this.bool = false;
}
}
The immutability of the internal state enables simple and side effect free programming. I suppose this is an invaluable functionality based on my experience on the other OOP languages (such as Java and Scala).
Very common in C# too. ReSharper (and I think now VS) both recommend you turn once-set internal fields into readonly fields.
For me, as long as the immutability ensured by read-only or getter-only doesn't stop at object references like the final keyword does in Java, then it would be tremendously helpful.
@dsebastien If an object publishes a method which alters its internal state, then what you mentioned does not help, I'm afraid.
someMethod(readonly myValue: string[]) {
myValue.length = 0; // if my understanding is correct, you want invalidate this, right?
myValue.push('str'); // even then, this can happen!
}
What really ensures object immutability is to make its internal state immutable, which is what Scala's immutable collections do, for example.
@kimamula I see what you mean but I guess it all depends on how far the TS compiler is willing to go :)
Like in C++, I think what is needed is a way to restrict the context to readonly. The compiler would then make sure that only immutable functions can be called on the context. So, something like:
someMethod(readonly myValue: string[]) readonly {
myValue.length = 0; // error because myValue is readonly and this is a property set
myValue.push('str'); // this would error because array.push is not marked as readonly (it mutates)
this.xxx = 0; // this would also error because someMethod is readonly
let x = myValue.length; // this would work - property get
let xx = myValue.map( p => p + this.xxx); // this would work - map is a readonly function accepting a readonly lambda
}
Note that the compiler would need to promote a lambda to a readonly lambda if it contains no context mutating operations.
@joewood With that particular example, I'm reminded of C#/JetBrains [Pure] attribute. Would be very interesting to only allow calling "Pure" functions or getters on a variable marked readonly.
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?