Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Value properties #2763

Closed
MrMatthewLayton opened this issue Apr 14, 2015 · 18 comments
Closed

Value properties #2763

MrMatthewLayton opened this issue Apr 14, 2015 · 18 comments
Labels
Out of Scope This idea sits outside of the TypeScript language design constraints Suggestion An idea for TypeScript

Comments

@MrMatthewLayton
Copy link

TypeScript currently supports accessor (get/set) properties, but ECMAScript 5 also allows value properties to be defined.

Object.defineProperty(o, p, { value: n, enumerable: true, configurable: false });

Will TypeScript be able to implement value properties in future?

@RyanCavanaugh RyanCavanaugh added the Question An issue which isn't directly actionable in code label Apr 17, 2015
@RyanCavanaugh
Copy link
Member

What is the use of this compared to o[p] = n?

@MrMatthewLayton
Copy link
Author

o[p] = n is always read/write

Value properties defined using Object.defineProperty can be defined to be readonly.

Object.defineProperty with just a getter would do the same thing, but that's not the point. TypeScript targets JavaScript, thus TypeScript should (in my opinion) be able to target every feature in JavaScript.

Food for thought...

@RyanCavanaugh
Copy link
Member

It's valid to call Object.defineProperty from TypeScript.

More information about how/when you would expect this to be emitted, and why, would be useful.

@MrMatthewLayton
Copy link
Author

It would be useful for implementing properties that are read-only, similar to the way int.MinValue, int.MaxValue and Point.Empty are implemented in the .NET framework (okay I know we're talking TypeScript right now, but the concepts are not dissimilar)

Consider the following example:

class Point {
    public Empty: Point;
    constructor(private x: number = 0, private y: number = 0) {
    }
    public get Area(): number { return this.x * this.y; }
}

Which emits this JavaScript:

var Point = (function () {
    function Point(x, y) {
        if (x === void 0) { x = 0; }
        if (y === void 0) { y = 0; }
        this.x = x;
        this.y = y;
    }
    Object.defineProperty(Point.prototype, "Area", {
        get: function () {
            return this.x * this.y;
        },
        enumerable: true,
        configurable: true
    });
    return Point;
})();

The TypeScript compiler is able to emit accessor properties because of TypeScript's get/set keywords, but it is not able to emit value properties at all. Granted; it is valid to call Object.defineProperty for value properties in TypeScript, but that doesn't automatically bring those properties into scope with TypeScript, and the compiler will error because it's not aware such a property exists.

The only way around this currently is to hand-roll an entire JavaScript class and use TypeScript definition files to work with the JavaScript - which isn't really a great use of TypeScript at all!

Consider the following example property syntax, which would not only allow TypeScript to emit value properties, it also allows the syntax for accessor properties to be more succinct than they are currently.

public X: number {
    set: (value) => { this.x = value; },
    get: () => { return this.x; }
}

public Empty: Point {
    value: new Point();
}

@RyanCavanaugh
Copy link
Member

There's #12 for read-only in the type system.

Can you define "value property" more exactly? Do you mean read-only? A property getter on the prototype of a class? Non-enumerability? Or just that it's important that the property be initialized on the object using Object.defineProperty instead of o[n] = x?

@MrMatthewLayton
Copy link
Author

Lets examine the facts here. If you study the MDN definition of Object.defineProperty you will see that there are two fundamental ways to define properties using Object.defineProperty.

  1. By using get/set accessor functions (TypeScript can emit these)
  2. By specifying "value" which can represent a mutable/immutable/read&write/readonly value (TypeScript CANNOT emit these)

To answer your questions...

Definition of "value property" - A property that is defined on an object or an object's prototype, which represents a value as documented in the MDN link above.

Do you mean read-only? yes, and no. You can specify writable: true/false on value properties to ensure that subsequent assignment attempts are allowed, or fail silently.

A property getter on the prototype of a class? No! This implies the use of an accessor property with only a get accessor function to ensure that the property cannot be assigned to.

Non-enumerability? - again, this can be specified using enumerable: true/false.

Or just that it's important that the property be initialized on the object using Object.defineProperty instead of o[n] = x? - In my opinion this is absolutely important. This is a new feature in ES5. It offers greater flexibility of properties that you get from o[p] = x (in terms of enumerability, configurability and writability). I think it is highly unfortunate that TypeScript does not currently take advantage of this feature.

In #12 you have stated "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." - I completely agree, but fundamentally, they are read-only in JavaScript, and value properties are one way (possibly the only way) JavaScript achieves this.

It appears from your suggestion that the "readonly" keyword would simply be another TypeScript compiler check, enforcing immutability of values. I am guessing that this would just disappear in the JavaScript, meaning that John Doe who comes along and makes some code that manipulates your immutable code will be able to do just that, since at this point TypeScript isn't there picking up Mr Doe's mistakes. ("value properties" are one way of solving this)

@mhegazy
Copy link
Contributor

mhegazy commented Apr 17, 2015

One option here is to use decorators. decorators allow you to change the definition of the property before it is applied to the class. so your example can look like:

function nonConfigurable(target, key) {
    // Make the property configurable=false
    Object.defineProperty(target, key, {
        enumerable: true,
        configurable: false
    });
}

class Point {
    @nonConfigurable
    public Empty: Point;
    constructor(x: number = 0) {
    }
}

you can do similar things with enumrable and writable as well.

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. and removed Question An issue which isn't directly actionable in code labels Apr 20, 2015
@MrMatthewLayton
Copy link
Author

Decorators will certainly bridge a gap in functionality, but I don't think this fully solves the issue.
In my opinion, there needs to be an overhaul in TypeScript's property syntax which will allow developers to take full advantage of ES5 properties.

Example - defining an accessor property with new syntax:

class AccessorExample {
    private: value: number = 0;

    public Value: number {
        get: () => { return this.value; },
        set: (value) => { this.value = value; },
        enumerable: true/false,
        configurable: true/false
    }
}

Emitted code:

var AccessorExample = (function () {
    function AccessorExample() {
        this.value = 0;
    }

    Object.defineProperty(AccessorExample.prototype, "Value", {
        get: function () { return this.value; },
        set: function (value) { this.value = value; },
        enumerable: true/false,
        configurable: true/false
    });

    return AccessorExample;
})();

Example - defining a value property with new syntax:

class ValueExample {    
    public Value: number {
        value: 123,
        enumerable: true/false,
        configurable: true/false,
        writable: true/false
    }
}

Emitted code:

var ValueExample = (function () {
    function ValueExample() {
    }

    Object.defineProperty(ValueExample.prototype, "Value", {
        value: 123,
        enumerable: true/false,
        configurable: true/false,
        writable: true/false
    });

    return ValueExample;
})();

So what have we achieved here?

  1. The new syntax is more concise and configurable
  2. The new syntax maintains a JavaScript-esque look and feel
  3. The new syntax eleviates the need for decorators, since configurable, enumerable and writable can be specified in the property declaration
  4. We can define both accessor properties and value properties
  5. We can define get/set functions using lambda syntax. This can already be specified in JavaScript edge mode
  6. So far, I have illustrated several compelling reasons why the syntax should change. Given my last point about using lambda syntax for accessor properties, please note that the following code compiles and runs in edge mode!

    var AccessorPropUsingLambda= (function () {
      function AccessorPropUsingLambda() {
        this.value = 0;
      }
    
      Object.defineProperty(AccessorPropUsingLambda.prototype, "Value", {
        get: () => { return this.x; },
        set: (value) => { this.x = value; },
        enumerable: true,
        configurable: true
      });
    
      return AccessorPropUsingLambda;
    })();
    

@mhegazy
Copy link
Contributor

mhegazy commented Apr 22, 2015

@series0ne, I actually like proposal; i think it fixes the ES separation of get/set that is very confusing in my opinion.

@irakliy81
Copy link

One quick question - should "enumerable" be somehow related to public/private declaration? So, for example, if a property is declared 'private', enumerable should be set to false.

@MrMatthewLayton
Copy link
Author

The two are not related. Remember that public/private declarations in TypeScript are simply compiler rules. Once the code is compiled to JavaScript, all that goes away; something that is private in TypeScript is actually public in JavaScript. Therefore I think the user should have complete control over enumerable, configurable and writable.

The way TypeScript treats public/private is another of my bugbears with the language...see #2940

@FlippieCoetser
Copy link

@series0ne , Great Proposal! Simplistic and Sensible... you have my vote.

@dsebastien
Copy link

+1 for @series0ne 's proposal

@rcollette
Copy link

Why limit the accessors to prototypes? In AngularJs, prototype accessors of a directive's controller are called before the controller's constructor. If an accessor needs to use an injected service, you have to abandon that first set call.

  var TestController1 = (function() {
    function TestController1(testService) {
      console.log("TestController1 constructor");
      this.testService = testService;
    }
    //This is how TypeScript generates properties.
    Object.defineProperty(TestController1.prototype, "aprop", {
      get: function() {
        console.log("TestController1 Get Value:",this._aProperty);
        return this._aProperty;
      },
      set: function(value) {
        console.log("TestController1 setter called with value: ", value);
        if(!this.testService){
          //in order to make this work I have to exit on the first call.
          console.log("TestController1: exiting, no service yet");
          return;
        }
        this._aProperty = this.testService.doSomething(value);
      },
      enumerable: true,
      configurable: true
    });
    return TestController1;
  })();

Instead I can define a property on _this captured in the constructor. In this case the accessor has access to the injected service it needs.

  var TestController2 = (function() {
    function TestController2(testService) {
      console.log("TestController2 constructor");
      var _this = this;
      this.testService = testService;

      //This is how I would typically create properties with angular
      Object.defineProperty(_this, "aprop", {
        get: function() {
          console.log("TestController2 Get Value:",this._aProperty);
          return this._aProperty;
          return _this._aProperty;
        },
        set: function(value) {
          console.log("TestController2 setter called with value: ", value);
          this._aProperty = _this.testService.doSomething(value);
        },
        enumerable: true,
        configurable: true
      });
    }
    return TestController2;
  })();

Console output:

script.js:61 TestController2 constructor
script.js:28 TestController1 setter called with value:  x
script.js:31 TestController1: exiting, no service yet
script.js:18 TestController1 constructor
script.js:72 TestController2 setter called with value:  y
script.js:8 service called with value:  y
script.js:28 TestController1 setter called with value:  x
script.js:8 service called with value:  x
script.js:68 TestController2 Get Value: Hello y
script.js:68 TestController2 Get Value: Hello y
script.js:24 TestController1 Get Value: Hello x
script.js:24 TestController1 Get Value: Hello x
script.js:68 TestController2 Get Value: Hello y
script.js:24 TestController1 Get Value: Hello x

See:
http://plnkr.co/edit/LDiKSzIJe2dQ7X3jS7V8?p=preview

@felixfbecker
Copy link
Contributor

We now have readonly and this can also be easily solved with a @nonconfigurable decorator.

@ricmoo
Copy link

ricmoo commented Jun 9, 2018

I am new to TypeScript and am currently porting my library over to it, but was surprised that readonly only applies to TypeScript checking. The generated JavaScript could also have the same safety meaning of readonly.

class Foo {
    readonly value: number;
    constructor(value: number) {
        this.value = value;
    }
}

Should compile to:

class Foo {
    constructor(value: number) {
        Object.defineProperty(this, 'value', { value: this.value, enumerable: true, writable: false });
        // Some compiler level additional check, that if value is not set by the end of the
        // constructor it is set to undefined; this must already happen for default assignment
    }
}

That way the JavaScript is also safe. Otherwise in JS, a user could easily do foo.value = "somethingElse".

Just my 2 cents. I've resorted to using Object.defineProperty manually in the constructors for readonly properties.

@felixfbecker
Copy link
Contributor

felixfbecker commented Jun 9, 2018

@ricmoo that would be against the design goals:

  1. Statically identify constructs that are likely to be errors.
  1. Impose no runtime overhead on emitted programs.
  1. Use a consistent, fully erasable, structural type system.

Non-goals:

  1. Add or rely on run-time type information in programs, or emit different code based on the results of the type system.

I.e. all type info should disappear upon compilation and should never cause different JS to be emitted. In theory, TS could also emit type validation checks with typeof for all function parameters, but that is a non-goal.

@RyanCavanaugh RyanCavanaugh added Out of Scope This idea sits outside of the TypeScript language design constraints and removed Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Jun 25, 2021
@RyanCavanaugh
Copy link
Member

Any further updates to fields should be taken to TC39.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Out of Scope This idea sits outside of the TypeScript language design constraints Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

9 participants