jayphelps / core-decorators

Library of stage-0 JavaScript decorators (aka ES2016/ES7 decorators but not accurate) inspired by languages that come with built-ins like @​override, @​deprecate, @​autobind, @​mixin and more. Popular with React/Angular, but is framework agnostic.
MIT License
4.52k stars 263 forks source link

Issues with readonly and enumreable #148

Closed JonWallsten closed 6 years ago

JonWallsten commented 6 years ago

I'm currently testing these decorators in a typescript boilerplate project, but I've run into some issues. I might totally miss understand how they work, in that case please enlighten me. When using read-only and nonenumerable on my props I can't get any values at all to stick. I also get nog warnings about the prop being read only when trying to set it.

Consider the following code:

export class Test {
  prop: string = 'Initial'

  @readonly
  propReadOnly: string = 'Initial'

  @nonenumerable
  propNonEnumerable: string = 'Initial'

  constructor() {
    console.log('Constructor: ', this);
    console.log('Log nonenumerable prop from within constructor: ', this.propNonEnumerable);
    console.log('Log read-only prop from within constructor: ', this.propReadOnly);
  }

  getProp(): string {
      return this.prop;
  }
  getNonEnumerableProp(): string {
      return this.propNonEnumerable;
  }
  getReadOnlyProp(): string {
      return this.propReadOnly;
  }

  setProp(value: string): void {
    this.prop = value;
  }
  setNonEnumerableProp(value: string): void {
    this.propNonEnumerable = value;
  }
  setReadOnlyProp(value: string): void {
    this.propReadOnly = value;
  }
}

Test

import { Test } from './test'

const test = new Test();

console.group('Class members:');
console.log(Object.keys(test));

console.groupEnd();

console.group('Prop');
console.log('test.prop (initial): ', test.prop);
console.log('test.getProp() (initial):', test.getProp());

test.prop = 'New value';
console.log('test.prop (assign): ', test.prop);
console.log('test.getProp() (assign): ', test.getProp());

test.setProp('Final value');
console.log('test.prop (set):', test.prop);
console.log('test.getProp() (set):', test.getProp());
console.groupEnd();

console.group('Prop Read-only');
console.log('test.propReadOnly (initial):', test.propReadOnly);
console.log('test.getReadOnlyProp() (initial):', test.getReadOnlyProp());

test.propReadOnly = 'New value';
console.log('test.propReadOnly (assign):', test.propReadOnly);
console.log('test.getReadOnlyProp() (assign):', test.getReadOnlyProp());

test.setReadOnlyProp('Final value');
console.log('test.propReadOnly (set):', test.propReadOnly);
console.log('test.getReadOnlyProp() (set):', test.getReadOnlyProp());
console.groupEnd();

console.group('Prop NonEnumerable');
console.log('test.propNonEnumerable: (initial)', test.propNonEnumerable);
console.log('test.getNonEnumerableProp(): (initial)', test.getNonEnumerableProp());

test.propNonEnumerable = 'New value';
console.log('test.propNonEnumerable (assign):', test.propNonEnumerable);
console.log('test.getNonEnumerableProp() (assign):', test.getNonEnumerableProp());

test.setNonEnumerableProp('Final value');
console.log('test.propNonEnumerable (set):', test.propNonEnumerable);
console.log('test.getNonEnumerableProp() (set):', test.getNonEnumerableProp());
console.groupEnd();

Output

Constructor:  
Test {prop: "Initial"} (when inspecting: prop: "Final value")
 Log nonenumerable prop from within constructor:  undefined
 Log read-only prop from within constructor:  undefined

Class members: Prop
 ["prop"]
 0: "prop"
 length: 1
 __proto__: Array(0)

Prop
 test.prop (initial):  Initial
 test.getProp() (initial): Initial
 test.prop (assign):  New value
 test.getProp() (assign):  New value
 test.prop (set): Final value
 test.getProp() (set): Final value

Prop Read-only
 test.propReadOnly (initial): undefined
 test.getReadOnlyProp() (initial): undefined
 test.propReadOnly (assign): undefined
 test.getReadOnlyProp() (assign): undefined
 test.propReadOnly (set): undefined
 test.getReadOnlyProp() (set): undefined

Prop NonEnumerable
 test.propNonEnumerable: (initial) undefined
 test.getNonEnumerableProp(): (initial) undefined
 test.propNonEnumerable (assign): undefined
 test.getNonEnumerableProp() (assign): undefined
 test.propNonEnumerable (set): undefined
 test.getNonEnumerableProp() (set): undefined
jayphelps commented 6 years ago

There are some known issues and non-standard ways that TS compiles decorators unfortunately.

core-decorators does not officially support TypeScript. There are known incompatibilities with the way it transpiles the output. PRs certainly welcome to fix that! - https://github.com/jayphelps/core-decorators#get-it

I haven't wanted to spend a bunch of effort on working around them because the decorator proposal in stage-2 changed the spec in very major ways that are incompatible and neither Babel nor TypeScript have implemented it yet.


That said, I'm not sure if that's related to your specific issues or not.

I also get nog warnings about the prop being read only when trying to set it.

If you're using the @readonly decorator, that should be expected. It doesn't make it readonly only for external accesses, it makes it readonly for everyone.

Perhaps you can elaborate? Sorry you're having issues!

JonWallsten commented 6 years ago

Sorry, we can close this. I got my answer from the official Typescript repo. https://github.com/Microsoft/TypeScript/issues/22048

jayphelps commented 6 years ago

@JonWallsten oh interesting. Thanks for following up!

eternalphane commented 5 years ago

@JonWallsten just fyi, I've found a way to decorate the instance property:

/**
 * @enumerable decorator that sets the enumerable property of a class field to false.
 * @param value true|false
 */
const nonenumerable = (target: any, propertyKey: string) => {
    let descriptor = Object.getOwnPropertyDescriptor(target, propertyKey) || {};
    if (descriptor.enumerable !== false) {
        Object.defineProperty(target, propertyKey, {
            enumerable: false,
            set(value: any) {
                Object.defineProperty(this, propertyKey, {
                    enumerable: false,
                    writable: true,
                    value
                });
            }
        })
    }
}

class Employee {
    @nonenumerable
    public id: number;
    public name: string;

    constructor() { 
        this.id = 1;
        console.log("this.id | Expected: 1 | Actual: ", this.id);
    }
}

var user = new Employee();
user.id = 2;
user.name = 'John Doe';
console.log("user.id | Expected: 2 | Actual: ", user.id);
console.log("Object.keys(user).length | Expected: 1 | Actual: ", Object.keys(user).length);

console.log("Object.keys(user): ", Object.keys(user));

(playground%0D%0A%20%20%20%20%7D%0D%0A%7D%0D%0A%0D%0Aclass%20Employee%20%7B%0D%0A%20%20%20%20%40nonenumerable%0D%0A%20%20%20%20public%20id%3A%20number%3B%0D%0A%20%20%20%20public%20name%3A%20string%3B%0D%0A%0D%0A%20%20%20%20constructor()%20%7B%20%0D%0A%20%20%20%20%20%20%20%20this.id%20%3D%201%3B%0D%0A%20%20%20%20%20%20%20%20console.log(%22this.id%20%7C%20Expected%3A%201%20%7C%20Actual%3A%20%22%2C%20this.id)%3B%0D%0A%20%20%20%20%7D%0D%0A%7D%0D%0A%0D%0Avar%20user%20%3D%20new%20Employee()%3B%0D%0Auser.id%20%3D%202%3B%0D%0Auser.name%20%3D%20'John%20Doe'%3B%0D%0Aconsole.log(%22user.id%20%7C%20Expected%3A%202%20%7C%20Actual%3A%20%22%2C%20user.id)%3B%0D%0Aconsole.log(%22Object.keys(user).length%20%7C%20Expected%3A%201%20%7C%20Actual%3A%20%22%2C%20Object.keys(user).length)%3B%0D%0A%0D%0Aconsole.log(%22Object.keys(user)%3A%20%22%2C%20Object.keys(user))%3B))