Code Monkey home page Code Monkey logo

Comments (132)

basarat avatar basarat commented on April 20, 2024 4

👍

from typescript.

everson avatar everson commented on April 20, 2024 1

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.

from typescript.

bgever avatar bgever commented on April 20, 2024 1

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

from typescript.

hdachev avatar hdachev commented on April 20, 2024 1

I think the const obj creates confusion in the example @IMPinball. Instead, I imagine noone would disagree that this must hold true:

class Foo {
  get foo() { return 1; }
}

var obj = new Foo();
obj.foo = 2; // Error

Read-only properties have been a matter of fact in javascript since the beginning of time, and it's unambiguous that type system support for read-only properties will be immensely helpful. Any other discussions about const objects and immutable collections IMHO are not relevant to this discussion.

Once we have read-only properties we could start discussing seriously stuff like modelling javascript "frozenness" and "sealedness" formally, but all our arguing about everyone's expectations of immutability and purity here are only detracting from the discussion. I seriously suggest we focus on getting type system support for read-only properties and think immutability next thing.

from typescript.

joelday avatar joelday commented on April 20, 2024 1

I really don't understand why this conversation is still going in circles. The spirit of the issue as a requested feature has nothing whatsoever to do with deep immutability. This is specifically nothing more than having a compiler error when we know at build time that, given the implicit or explicit interface of an object, an assignment is absolutely guaranteed to always cause a runtime error.

I say this with full appreciation for the fact that syntax changes merit very serious scrutiny. I just propose that other immutability features should be forked to a different issue and considered OT here.

from typescript.

erfangc avatar erfangc commented on April 20, 2024 1

Perhaps we can explore and borrow the idea of case classes from Scala
i.e.

immutable class Point {
    public x: number;
    public y: number;
}

where x and y are automatically readonly, all immutable classes are provided a copy() or clone() methods for deep and shallow copying

if a particular property needs to be mutable for any reason, simply denote it with var i.e. public var lastModified: Date

so that's +1 for immutable class for me

from typescript.

RyanCavanaugh avatar RyanCavanaugh commented on April 20, 2024

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;

from typescript.

joewood avatar joewood commented on April 20, 2024

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

from typescript.

RyanCavanaugh avatar RyanCavanaugh commented on April 20, 2024

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

from typescript.

joewood avatar joewood commented on April 20, 2024

Sorry, misread the status.

from typescript.

Jxck avatar Jxck commented on April 20, 2024

+1 for readonly keyword support.

from typescript.

IanYates avatar IanYates commented on April 20, 2024

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

from typescript.

RandScullard avatar RandScullard commented on April 20, 2024

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

from typescript.

danpaul88 avatar danpaul88 commented on April 20, 2024

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

from typescript.

Eyas avatar Eyas commented on April 20, 2024

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

from typescript.

joewood avatar joewood commented on April 20, 2024

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.

from typescript.

metaweta avatar metaweta commented on April 20, 2024

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.

from typescript.

Eyas avatar Eyas commented on April 20, 2024

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.

from typescript.

Lenne231 avatar Lenne231 commented on April 20, 2024

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

from typescript.

Lenne231 avatar Lenne231 commented on April 20, 2024

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) { }
}

from typescript.

gustavderdrache avatar gustavderdrache commented on April 20, 2024

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.

from typescript.

Lenne231 avatar Lenne231 commented on April 20, 2024

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

from typescript.

Eyas avatar Eyas commented on April 20, 2024

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.

from typescript.

gustavderdrache avatar gustavderdrache commented on April 20, 2024

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

from typescript.

basarat avatar basarat commented on April 20, 2024

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.

from typescript.

StarTether avatar StarTether commented on April 20, 2024

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?

from typescript.

irakliy avatar irakliy commented on April 20, 2024

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

from typescript.

Eyas avatar Eyas commented on April 20, 2024

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.

from typescript.

irakliy avatar irakliy commented on April 20, 2024

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

from typescript.

irakliy avatar irakliy commented on April 20, 2024

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

from typescript.

Ciantic avatar Ciantic commented on April 20, 2024

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.

from typescript.

mhevery avatar mhevery commented on April 20, 2024

👍

from typescript.

toddwong avatar toddwong commented on April 20, 2024

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

from typescript.

dead-claudia avatar dead-claudia commented on April 20, 2024

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.

from typescript.

IngageStroliaC avatar IngageStroliaC commented on April 20, 2024

👍

from typescript.

dsebastien avatar dsebastien commented on April 20, 2024

+1

from typescript.

teintinu avatar teintinu commented on April 20, 2024

+1

from typescript.

drake7707 avatar drake7707 commented on April 20, 2024

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

from typescript.

Justin-Credible avatar Justin-Credible commented on April 20, 2024

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

from typescript.

joelday avatar joelday commented on April 20, 2024

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

from typescript.

drake7707 avatar drake7707 commented on April 20, 2024

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.

from typescript.

hdachev avatar hdachev commented on April 20, 2024

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.

from typescript.

joelday avatar joelday commented on April 20, 2024

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

from typescript.

RandScullard avatar RandScullard commented on April 20, 2024

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

from typescript.

dead-claudia avatar dead-claudia commented on April 20, 2024

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 [email protected] 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
#12 (comment)
.

from typescript.

joelday avatar joelday commented on April 20, 2024

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

from typescript.

kimamula avatar kimamula commented on April 20, 2024

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

from typescript.

RichiCoder1 avatar RichiCoder1 commented on April 20, 2024

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

from typescript.

dsebastien avatar dsebastien commented on April 20, 2024

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.

from typescript.

kimamula avatar kimamula commented on April 20, 2024

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

from typescript.

dsebastien avatar dsebastien commented on April 20, 2024

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

from typescript.

joewood avatar joewood commented on April 20, 2024

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.

from typescript.

RichiCoder1 avatar RichiCoder1 commented on April 20, 2024

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

from typescript.

drake7707 avatar drake7707 commented on April 20, 2024

@joewood While bad practice, nothing prevents a user to implement a property getter that has side effects. (unless ofcourse you can also annotate its signature as readonly)

from typescript.

joewood avatar joewood commented on April 20, 2024

I think the compiler could warn on that. If the compiler auto promotes any function (including property getters) to read-only if they don't mutate then the compiler can warn if the readonly context modifier is applied and contradicts with what the compiler sees.

Of course, the problem with this approach will be like in C++ with const. One bad apple can spoil the whole thing.

from typescript.

tomitrescak avatar tomitrescak commented on April 20, 2024

Is it possible to add a check to compiler similar to "suppressImplicitAnyIndexErrors": true" that would check assignment to "get-only" properties? I think that is a good start that will cover 95% of its use cases. Immutability could be solved as a separate task. How about "checkReadonlyProperties"? I am probably reinventing hot water, but I think this would be extremely useful for many of TS programmers.

from typescript.

dead-claudia avatar dead-claudia commented on April 20, 2024

@tomitrescak That could just as easily be solved by marking getters as readonly.

from typescript.

tomitrescak avatar tomitrescak commented on April 20, 2024

@IMPinball And how can that be done? Specifying only getter and assigning value to such getter passes compilation.

from typescript.

dead-claudia avatar dead-claudia commented on April 20, 2024

Or at least a constant reference. Something to that effect.

from typescript.

tomitrescak avatar tomitrescak commented on April 20, 2024

@IMPinball you are being very confusing. How can you mark a getter as readonly so that assiging value to it will not pass compilation?

In another words, how do you make follwing fail compilation?

class MyClass {
    private theCount: number;
    private theOther: number;

    public get count() {
        return this.theCount;
    }

    public set other(value: number) {
        this.theOther = value;
    }
}

var c = new MyClass();
c.count = 5; // should fail

var e = c.other; // should fail

from typescript.

dead-claudia avatar dead-claudia commented on April 20, 2024

@tomitrescak I mean in the sense of it being a glorified constant.

const foo = 2;
foo = 1; // Error

class Foo {
  get foo() { return 1; }
}

const obj = new Foo();
obj.foo = 2; // Error

from typescript.

tomitrescak avatar tomitrescak commented on April 20, 2024

Well, that's not what I was proposing :/, but thanks for your insight! Sorry for hijacking the discussion.

from typescript.

bgever avatar bgever commented on April 20, 2024

Defining an object as a constant is a hack @IMPinball, I agree with @tomitrescak that assignments to getters should fail compilation. Same for reading a getter. Moreover, an interface should be able to enforce these constraints.

@gustavderdrache nailed that in comment: #12 (comment)

from typescript.

dead-claudia avatar dead-claudia commented on April 20, 2024

Okay... I'll admit the similarity with const would be a little bit
hackish. But last time I checked, you can't assign to either one, const
or getter without setter. You can get the value of either one, but they
can't be assigned to. Because they behave identically on the left hand side
(excluding getter side effects), that's why I suggested it.

As for const obj vs var obj, I typed const out of habit. Any of the
three declarations would suffice.

On Fri, Sep 18, 2015, 05:26 Hristo Dachev [email protected] wrote:

I think the const obj creates confusion in the example @IMPinball
https://github.com/impinball. Instead, I imagine noone would disagree
that this must hold true:

class Foo {
get foo() { return 1; }
}
var obj = new Foo();
obj.foo = 2; // Error

Read-only properties have been a matter of fact in javascript since the
beginning of time, and it's unambiguous that type system support for
read-only properties will be immensely helpful. Any other discussions about
const objects and immutable collections IMHO are not relevant to this
discussion.

Once we have read-only properties we could start discussing seriously
stuff like modelling javascript "frozenness" and "sealedness" formally, but
all our arguing about everyone's expectations of immutability and purity
here are only detracting from the discussion. I seriously suggest we focus
on getting type system support for read-only properties and think
immutability next thing.


Reply to this email directly or view it on GitHub
#12 (comment)
.

from typescript.

 avatar commented on April 20, 2024

This is now Typescript 1.6 available and still no support for this feature. You even implemented a support for third party library like the React in this version and still no long awaited and long discussed support for the compiler to check that the property has no setter. I had a bug in my project because the compiler did not give any hint while i assigned values to readonly properties.

Could you please make this feature in the compiler because it is needed for large projects like mine
https://github.com/BBGONE/jRIAppTS

from typescript.

 avatar commented on April 20, 2024

I suggest a quick and effective solution.
As a compromise you can allow implicit casting immutable => mutable is OK.
Just don't allow assignment to a readonly property when the variable has type of the interface with that property marked readonly.
It is needed because developers who create frameworks define interfaces in it which can be consumed by clients and the clients should be aware what property is readonly and which is not.
It is up to them to follow the contract what the developers provided.
For example: an Entity can have calculated property and the clients should be aware that the property is readonly.
It will cover 99% of usecases. Very litle developers care about the issues that a function can accept an argument and modify it internally. Most of the problem is when you work with properly typed variables (with readonly properties) and there's an assignment to it in the code.

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; // immutable => mutable ALLOW IT!
pt2.x = 6; // ALLOW IT because it is now mutable
pt= pt2 //IT is again immutable
pt.x = 7; // Error, 'pt.x' is not a valid target of assignment

P.S. - reclassify this issue as a bug, because technically it is a bug : the compiler should check that a property can not be wriitten because a developer explicitly did not provide a setter.

from typescript.

dead-claudia avatar dead-claudia commented on April 20, 2024

@BBGONE

Only the part regarding getters without setters could remotely be considered a bug. The rest cannot, such as the readonly modifier for other types. The feature didn't exist before, and it's not directly mis-typing the language. And in case you're curious, even the mis-typing of this was considered a suggestion, not a bug. That problem came down to JavaScript being call-by-delegation in its object model, like Lua or Python, vs TypeScript typing it like a language with statically known methods like Java or C# (which isn't accurate at all).

from typescript.

 avatar commented on April 20, 2024

@IMPinball
"even the mis-typing of this was considered a suggestion, not a bug."
It is considered a suggestion because it has an easy workaround and i use it often:
instead of untyped 'this' use a typed 'self'

function f() {
var self: SomeType = this;
self.x = 3;
}

But the issue with ReadOnly properties have no workaround.
And my proposal can easily be implemented transparently to the existing codebase.
Developers can then use interfaces with the new features, and their code will be protected from that bug if they use interfaces and typing consistently. Third party libs can be left as is.

from typescript.

dead-claudia avatar dead-claudia commented on April 20, 2024

@BBGONE

The thing with this is that even Function.prototype.call is improperly typed. The standard library doesn't correctly take this into account.

As for readonly properties, it's still a feature. It's not a feature that was improperly/incorrectly implemented. Those constitute actual bugs.

from typescript.

dead-claudia avatar dead-claudia commented on April 20, 2024

@joelday I agree with the entirety of your statement.

Back to the initial discussion, I do believe that there needs to be a way of marking whether something can be read or written. This would be an awesome thing to be able to have:

interface LodashArray {
  readonly length: number;
}

There's no way I can possibly modify the length without something going wrong. Also, assigning to getters without setters should also fail in this case:

class Foo {
  get foo() { return 2 }
}

new Foo().foo = 3 // error!
let foo = new Foo().foo // okay

Also, there should be a way to properly type setters without getters. I know it's not very common and not very elegant, but JS and TS both allow it, so it should be checked, anyways.

class Foo {
  set foo(value) { /* ... */ }
}

let foo = new Foo().foo // error!
new Foo().foo = 2 // okay

from typescript.

dead-claudia avatar dead-claudia commented on April 20, 2024

@jbondc You mean s/writable/readable/g and s/mutable/writable/g? ;)

Also, what about get T and set T? There is no existing ambiguity AFAIK, since there's just whitespace between the two tokens.

from typescript.

dead-claudia avatar dead-claudia commented on April 20, 2024

Ok. It really confused me. To be honest, I don't like that naming for that
very reason: it's non-obvious.

On Mon, Oct 19, 2015, 08:36 Jon [email protected] wrote:

@IMPinball https://github.com/impinball Yes well:

  • !writable reads as Not Writable (readable).
  • !mutable reads as Not Mutable (constant, immutable, ..?)

I explain it more here about attaching constraints to a type:
#3076 (comment)
#3076 (comment)


Reply to this email directly or view it on GitHub
#12 (comment)
.

from typescript.

RandScullard avatar RandScullard commented on April 20, 2024

I would read number!writable as "number that is writable", and number!writable!enumerable as "number that is writable and enumerable". Is that what you meant?

from typescript.

joewood avatar joewood commented on April 20, 2024

Couple of thoughts:

  • Wouldn't writable be better defined by simply the existence of the set property function? I realize that these are defined separately in JavaScript - but that makes more logical sense to me.
  • For other property metadata (like configurable and enumerable), rather than creating a new syntax couldn't decorators be repurposed for this? I mean using some well recognized decorators defined internally to the compiler?

from typescript.

jbondc avatar jbondc commented on April 20, 2024

Alternative I guess could be:

declare class Foo {
   someValue: number;
   foo: number{writable: false, enumerable: false}
}

@joewood Properties are writable in JS so not sure what you meant by the set property:

class Foo {
  someValue = 1
  set foo() { this.someValue++ }
}

The emit in a declaration file would be:

declare class Foo {
   someValue: number;
   foo: number;
}

With decorators, look at the generated code:
http://www.typescriptlang.org/Playground#src=%40A%0Aclass%20Clazz%20{%0A%20%20%20%20%40B%0A%20%20%20%20prop%20%3D%201%3B%0A%0A%20%20%20%20%40C%0A%20%20%20%20method%28%29%20{}%0A%0A%20%20%20%20%40D%0A%20%20%20%20get%20prop%28%29%20{return%201%3B}%0A%0A%20%20%20%20%40E%0A%20%20%20%20method2%28%29%20{}%0A%0A%20%20%20%20%40F%0A%20%20%20%20prop2%20%3D%201%3B%0A}

Not something I'm particularly excited about. They are a runtime thing and can't be applied to types/interfaces etc...

e.g.

function enumerable(proto, name) {
    Object.defineProperty(proto, name, { value: undefined, enumerable: true });
}

class Foo {
    @enumerable
    a: number;
}

The emit in a declaration file is:

declare function enumerable(proto: any, name: any): void;
declare class Foo {
    a: number;
}

So you lose the info.

from typescript.

joewood avatar joewood commented on April 20, 2024

@jbondc I mean if the emit included a declaration for the getter/setter. The compiler would then error if you were trying to use the property as an lvalue in an expression. This seems more natural to me, unless I'm missing something.

For decorators - I'm talking about reusing the syntax as a way of adding metadata, not as a runtime evaluation or generating any code. So this would just be used for metadata exposed to the compiler.

from typescript.

jbondc avatar jbondc commented on April 20, 2024

So you mean:

class Foo {
  someValue = 1
  get foo() { return this.someValue }
}

Declaration is:

declare class Foo {
   someValue: number;
   get foo: number;
}

Guess that can work too, syntax is more a game of perspective.

from typescript.

joewood avatar joewood commented on April 20, 2024

Right, seems to be more inline with the goal of declaration files being the header files of JavaScript.

from typescript.

dead-claudia avatar dead-claudia commented on April 20, 2024

And this is why I suggested get and set modifiers (I don't care if it's
prefixing the type or value name - it reads the same). I think it would
make perfect sense. The intent is very clear in that you can get and/or
set the property.

// Prefixing the name
declare class Foo {
  someValue: number;
  get foo: number;
}

// Prefixing the type
declare class Foo {
  someValue: number;
  foo: get number;
}

I'm indifferent to which is used, although I suspect most here would prefer
the former.

On Mon, Oct 19, 2015 at 11:46 AM, Joe Wood [email protected] wrote:

Right, seems to be more inline with the goal of declaration files being
the header files of JavaScript.


Reply to this email directly or view it on GitHub
#12 (comment)
.

Isiah Meadows

from typescript.

jbondc avatar jbondc commented on April 20, 2024

Gotcha makes sense, like it.

from typescript.

joelday avatar joelday commented on April 20, 2024

@IMPinball Thanks! I think the issue with using "readonly" as a keyword in an interface is that it can be interpreted as declaring that the property cannot be writable on an object that implements that interface. Also, I think it would get confusing (or plain not work in some situations) when extending interfaces, and class declarations of get/set properties is also fundamentally additive rather than negative, so I think it would make sense for declaring it on interfaces to work the same way and use the same keywords.

from typescript.

joelday avatar joelday commented on April 20, 2024

Quick reminder that I prototyped what we're talking about a while ago: https://github.com/joelday/TypeScript/blob/getSetKeywordAssignment/tests/cases/compiler/assignmentToReadOnlyMembers.ts

:)

from typescript.

Artazor avatar Artazor commented on April 20, 2024

Any progress?

from typescript.

falsandtru avatar falsandtru commented on April 20, 2024

+1 for final modifier.

from typescript.

Avol-V avatar Avol-V commented on April 20, 2024

This discussion not about constants in classes, but some issues marked as duplicate of this, and I wont to show my vision about this (it also gives an option for this #3743 ).
We can do something like this:

class MyClass
{
    static const MY_STATIC_CONST: string = 'my staic const';
    const MY_CONST: string = 'my const';
}

And generated JS:

Object.defineProperty(
    MyClass,
    'MY_STATIC_CONST',
    {
        value: 'my staic const',
        enumerable: true
    }
);
Object.defineProperty(
    MyClass.prototype,
    'MY_CONST',
    {
        value: 'my const',
        enumerable: true
    }
);

It looks simple and useful for me.

from typescript.

 avatar commented on April 20, 2024

Will we see a solution of this implemented in typescript compiler?
P.S. I need a solution for the bug when you can assign a value to a property without setter and
proper expression of this in the interfaces.

talk, talk , talk ....

from typescript.

Avol-V avatar Avol-V commented on April 20, 2024

TypeScript already generates defineProperty for ES5 getters/setters.
And, as I have shown, in this case we can declare properties for prototype, that is also important.

from typescript.

ahejlsberg avatar ahejlsberg commented on April 20, 2024

I'm going to take a look at implementing something here. My current thoughts are:

  • Introduce a readonly modifier that can be applied to property and method declarations in interfaces and classes, and infer a read-only property when a class includes only a get accessor declaration for a particular property.
  • Allow the readonly modifier to also be applied to interface and class declarations, making all members introduced in that interface or class declaration read-only.
  • Disallow assignments to read-only properties, except for property initializers in classes and property assignments in constructors.
  • In subtyping and assignment compatibility checks, allow a read-write property to be treated as a read-only property, but not vice versa.

This effectively amounts to "shallow" immutability. With that in place, deeply immutable object types can be created by manually ensuring that all properties are read-only properties of primitive types (string, number, or boolean) or object types that are themselves deeply immutable (transitively).

I think this would cover the type checking side of things. I'm not sure we really want this to affect code generation (e.g. by using Object.defineProperty) since there is overhead associated with that.

from typescript.

kitsonk avatar kitsonk commented on April 20, 2024

This effectively amounts to "shallow" immutability.

Isn't that shallow immutability logic the same logic as const, therefore relatively straight forward to guard against?

from typescript.

joewood avatar joewood commented on April 20, 2024

Could readonly also be applied to parameter types as a type modifier providing a read-only reference to an existing object? Only getters and readonly members would then be available to that function. e.g.:

function doThis( someObject: readonly Foo ) {
    someObject.prop = 123; // error
    var v = someObject.prop; // ok
} 

In a similar way to C++ const. Without a type-modifier I suspect we'll end up writing interface definitions twice, once for immutability and again for mutability.

from typescript.

ahejlsberg avatar ahejlsberg commented on April 20, 2024

@kitsonk The difference between const and readonly is that it isn't possible to alias a const and therefore you're guaranteed it will never be mutated. With readonly you can fairly easily create a type with all read-only properties that aliases an object with read-write properties. In that scenario it is still possible for mutations to happen, it's just that you can't cause them.

@joewood The issue with readonly as a type modifier is that it may not do what you expect. For example, a readonly string[] would still have a push method that mutates the array. All readonly would do is keep you from assigning to the properties of the array.

from typescript.

joewood avatar joewood commented on April 20, 2024

@ahejlsberg maybe I not following, but wouldn't push be inaccessible because it wouldn't be marked as readonly because it mutates. Whereas functions like slice would have a readonly modifier, or doesn't it work like that.

from typescript.

RyanCavanaugh avatar RyanCavanaugh commented on April 20, 2024

It's probably worth breaking down several of the concepts of readonlyness that some languages have so we can clarify which would and would not exist:

  1. Is the object I'm referencing mutable or immutable ? i.e. am I allowed to do things to this object which mutate its state, via this reference?
  2. Is this reference (primitive or otherwise) a mutable reference or an immutable reference? i.e. am I allowed to change this reference to point to a different object (or different value)?
  3. Is a given property of an object ever settable, even when the object is mutable?

Using C++ as the most direct analogy, we can understand the difference between 1 and 2 as the difference between const int* x and int* const x. Since C++ doesn't have property getters/setters, point 3 reduces to point 1 (modulo the mutable keyword).

Since C++ doesn't have the ability to set e.g. std::vector.insert on an arbitrary object reference, it doesn't have to make any subtle distinctions that a hypothetical JavaScript-based full understand of mutability would require. To clarify: there are two concepts you might want to express about Array#indexOf or Array#push -- you don't want to allow arbitrary setting of those properties, but only the latter causes mutation of the underlying object when invoked. It's worth thinking about how std::vector.size() is a method, rather than a field, in that context (you can't set the size of a mutable std::vector via that member).

What's on the table (as far as I understand) is the following:

  • You can declare that object properties are not settable. For example, setting the length of a Function does nothing and should be a compile-time error.
  • You cannot declare or determine that a method does or does not alter the state of an object. For example, calling someArray.slice() (which does not modify the object) and someArray.reverse() (which does) are indistinguishable.
  • You can declare that all fields of a class or interface are read-only, but this is just a shortcut for declaring each property of that type with the readonly modifier.
  • Existing behavior of const vs. let/var cover reference immutability for variables

Since now I've written all this and now realize that there's a much simpler explanation, consider this symmetry:

const items: string[] = []; // OK, initializer
items.push('ok'); // OK
items = []; // Nope, const reference

and

readonly class MyClass {
  items: string[]; // readonly because containing class is readonly
  constructor() {
    this.items = []; // OK, initializer
  }
}
let m1 = new MyClass();
m1.items.push('hello'); // OK
m1.items = []; // Nope, readonly property
m1 = new MyClass(); // OK, not a const reference

const m2 = new MyClass();
m2.items = []; // Still an error
m2.items.push('world'); // Still OK
m2 = new MyClass(); // Nope, const reference

from typescript.

kitsonk avatar kitsonk commented on April 20, 2024

...and if the user wanted more run-time immutability they could:

readonly class MyClass {
  items: string[]; // readonly because containing class is readonly
  constructor() {
    this.items = []; // OK, initializer
    Object.freeze(this.items);
  }
}

const m3 = new MyClass();
m3.items.push('world'); // Throws an error in strict mode (or noop otherwise)

TypeScript hasn't "tracked" that type of immutability to date and I suggest the proposal for readonly also would just leave that behaviour to the runtime?

from typescript.

Avol-V avatar Avol-V commented on April 20, 2024

@ahejlsberg
Maybe your readonly modifier is not the same as const/final modifier in classes, that me and some other people expect.
I'm looking for ability of creating constants in classes such as in other languages and as we can see in javascript standard objects: Node.ELEMENT_NODE, XMLHttpRequest.DONE and other (in js we also can have access to these constants from instances, because of prototypes, but it's not really needed).
And these constants in js is read-only — I just want to have ability to declare such constants in TypeScript easely.

But, maybe, your readonly is OK, I have only one question — why you don't want to make class instance read-only properties assigned once for prototype and not for each instance?

from typescript.

raveclassic avatar raveclassic commented on April 20, 2024

Such a long discussion and finally things become clearer!

@RyanCavanaugh All that's on the table is correct but there's a small thing I want to mention about - the difference in getter/setter accessors visibility.

I agree that an object property can have no setter:

const foo = {
    get bar() {
        return 0;
    }
}

foo.bar = 1; //error

I think this is clear. But here's a bit more complex example that describes the problem:

declare abstract class Emitter {
    new (): Emitter;
    on: (name:string, handler: (arg:any) => void) => void;
    off: (name:string, handler: (arg:any) => void) => void;
    protected emit: (name:string, arg:any) => void;
}

class Person extends Emitter {
    constructor(name:string) {
        super();
        this.name = name;
    }

    private _name:string;
    get name() {
        return this._name;
    }
    set name(value) {
        if (this._name !== value) {
            this._name = value;
            this.emit('change:name', value);
            //this way is better
            this.updatedAt = new Date();
            //than this way
            this.setUpdatedAt(new Date());
        }
    }

    ////
    private _updatedAt:Date;
    get updatedAt() {
        return this._updatedAt;
    }
    private set updatedAt(value) { //Getter and setter do not agree in visibility
                //some logic and a simple readonly (our absence of a setter) is not enough
        if (this._updatedAt !== value) {
            this._updatedAt = value;
            this.emit('change:updatedAt', value);
        }
    }

    //// method implementation - but what's the point in it?
    private setUpdatedAt(value) {
        if (this._updatedAt !== value) {
            this._updatedAt = value;
            this.emit('change:updatedAt', value);
        }
    }
}

const entity = new Person('Mike');
entity.on('change:updatedAt', console.log.bind(console));
entity.name = 'Thomas';
//but manually setting updatedAt should be forbidden
entity.updatedAt = new Date(); //restricted

Here, property updatedAt actually can have a setter but it should not be accessable outside of Person. Moreover this setter contains complex logic and a simple readonly or an absence of a setter is not enough.

I agree that private setter is just a syntax sugar for private methods with additional logic (like emitting) but I think it is inconsistent when part of logic related to a field is in a property (getter) and another in a method (actual private setter).

This way getters/setter are implemented in C#, I think it would be usefull to allow different visibility at a compile time, though this contracts will be lost in runtime.

UPDATE I've found the duplication of my suggestion: #2845 but it is linked and closed

from typescript.

joewood avatar joewood commented on April 20, 2024

@RyanCavanaugh yes, understood. My two requests are really:

Type Modifier
Not having a type modifier on a reference (no. 2 above on your list) will mean that if you want to pass around an immutable reference to an object you will end up having to duplicate the interface definition with a simple readonly before the declaration. That seems a little frustrating, especially given how popular immutability is becoming on the client side. So something like this (even though it's only shallow)

interface Foo {
   x: number;
   y: Bar;
}

function doSomething( foo: readonly Foo ) { // or maybe using the const keyword
   foo.x = 10; // error
   var x = foo.x; // ok
   foo.y.bar = 10; // unfortunately ok because only shallow
}

Note, without this the developer would need to declare Foo and also a duplicate interface ImmutableFoo.

Context Type Modifier
Second request is to be able to decorate a function that acts as a type modifier for this. Property getters have this set implicitly.

interface Foo {
    readonly getState() : number;
    resetState();
}

function readFoo( foo: readonly Foo ) {
    let state = foo.getState(); // OK, works as readonly context function
    resetState(); // error - mutating function
}

With a type modifier you could define a getter that returns a readonly type, so the immutability isn't just shallow. I think the interface definition would need to be extended to support that, or add some sort of immutable directive to the type definition.

from typescript.

raveclassic avatar raveclassic commented on April 20, 2024

@Avol-V
Both Node.ELEMENT_NODE, XMLHttpRequest.DONE are not constants but enums which are immutable be design and you can just export them from the module along with the class using them - this is the proper way in ES6/TS.

If you need an immutable property on the class you can define it with only a getter:

export enum NodeType {
    ELEMENT_NODE,
    SOME_OTHER_NODE_TYPE
}

export class SomeElement {
    private _type:NodeType;
    get type() {
        return this._type;
    }
}

This works right now except of the ability to write to a property without a setter which was considered a bug eariler in the conversation. No readonly/final/immutable etc.

from typescript.

Avol-V avatar Avol-V commented on April 20, 2024

@raveclassic
No, Node and XMLHttpRequest and others — is a classes, not enums. And ELEMENT_NODE and others — is constant in namespace of this class. Your example is wrong.
Correct example is:

class Node
{
    
}
namespace Node
{
    export const ELEMENT_NODE: number = 3;
}

But in this case I have troubles with inheritance.

from typescript.

raveclassic avatar raveclassic commented on April 20, 2024

@Avol-V Yes, you are right, misunderstood. I can see now that your point is about inheriting class constants.
The problem here is that this issue is bloated and should be split. We are all talking about different things.

from typescript.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.