microsoft / TypeScript

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

Suggestion: read-only modifier #12

Closed RyanCavanaugh closed 8 years ago

RyanCavanaugh commented 10 years ago

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:

interface Point {
    x: number;
    y: number;
}
interface ImmutablePoint {
    readonly x: number;
    readonly y: number;
}
var pt: ImmutablePoint = { x: 4, y: 5 }; // OK, can convert mutable to non-mutable
pt.x = 5; // Error, 'pt.x' is not a valid target of assignment

var pt2: Point = pt; // Error, cannot convert readonly 'x' to mutable 'x'

// Possibly bad behavior
var pt3: Point = { x: 1, y: 1 };
var pt4: ImmutablePoint = pt3; // OK
pt3.x = 5; // pt4.x is also changed?

// Really bad behavior
/** This function was written in TypeScript 1.0 **/
function magnitudeSquared(v: { x: number; y: number }) {
   return v.x * v.x + v.y * v.y;
}
// Now try to use it with ImmutablePoint
console.log(magnitudeSquared(pt)); // Error, cannot use readonly object in non-readonly call

Possible solutions?

everson commented 10 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.

basarat commented 10 years ago

:+1:

RyanCavanaugh commented 10 years ago

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

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 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;

Assume all function arguments are actually mutable?

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;
joewood commented 10 years ago

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).

RyanCavanaugh commented 10 years ago

I'm not sure what you're asking? The issue is currently open.

joewood commented 10 years ago

Sorry, misread the status.

Jxck commented 9 years ago

+1 for readonly keyword support.

IanYates commented 9 years ago

+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)?

RandScullard commented 9 years ago

"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.

danpaul88 commented 9 years ago

+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).

Eyas commented 9 years ago

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:

  1. Unclear what to do about deep properties; I'm tempted to say "nothing" here... the immutability guarantee might be seen as a guarantee to not change references to other objects (values of pointers), rather than values within these objects.
  2. For the magnitudeSquared problem, we'd need to declare it as magnitudeSquared(v: immutable v { x: number, y: number }).
joewood commented 9 years ago

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.

metaweta commented 9 years ago

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.

Eyas commented 9 years ago

Perhaps even more explicit, another suggestion similar to my previous one:

No clone & freeze, simply, immutable {...} can only be assigned by types that are:

  1. Structurally compatible, and
  2. Also immutable.

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.

Lenne231 commented 9 years ago

:+1: I really like Eyas' suggestion, but the .toImmutable(); function should not be part of TypeScript.

Lenne231 commented 9 years ago

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) { }
}
gustavderdrache commented 9 years ago

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:

  1. const bindings, as offered by ES6,
  2. Get-only computed properties,
  3. Ordinary properties configured with writable: false in their descriptors, and
  4. Objects passed through Object.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.

Lenne231 commented 9 years ago

There is also an EcmaScript 7 proposal on immutable data structures: https://github.com/sebmarkbage/ecmascript-immutable-data-structures

Eyas commented 9 years ago

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.

gustavderdrache commented 9 years ago

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.)

basarat commented 9 years ago

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.

StarTether commented 9 years ago

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?

irakliy commented 9 years ago

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:

Declarations

interface Point {
    x: number,
    y: number
}

// declaring readonly properties is done with #
interface ImmutablePoint {
    #x: number,
    #y: number
}

Assignments

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

Function Calls

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
Eyas commented 9 years ago

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.

irakliy commented 9 years ago

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
irakliy commented 9 years ago

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.

read-only interface

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

read-only data

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
Ciantic commented 9 years ago

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.

mhevery commented 9 years ago

:+1:

toddwong commented 9 years ago

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);
}
dead-claudia commented 9 years ago

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.

IngageStroliaC commented 9 years ago

:+1:

dsebastien commented 9 years ago

+1

teintinu commented 9 years ago

+1

drake7707 commented 9 years ago

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 :/

bgever commented 9 years ago

I agree with @drake7707, this was my main reason to start following this thread.

Justin-Credible commented 9 years ago

:+1: for @drake7707's idea; also the reason I am following this thread.

joelday commented 9 years ago

@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.

drake7707 commented 9 years ago

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.

hdachev commented 9 years ago

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.

joelday commented 9 years ago

@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.)

RandScullard commented 9 years ago

@joelday I love where you're going with this.

dead-claudia commented 9 years ago

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 .

joelday commented 9 years ago

@RandScullard & @impinball Glad we're on the same page. :D

kimamula commented 9 years ago

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).

RichiCoder1 commented 9 years ago

Very common in C# too. ReSharper (and I think now VS) both recommend you turn once-set internal fields into readonly fields.

dsebastien commented 9 years ago

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.

kimamula commented 9 years ago

@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.

dsebastien commented 9 years ago

@kimamula I see what you mean but I guess it all depends on how far the TS compiler is willing to go :)

joewood commented 9 years ago

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.

RichiCoder1 commented 9 years ago

@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.