Search code examples
typescripttypescript-decorator

Why does a property created with Object.defineProperty in a decorator not show up on the object?


I want to validate the value of a property when it is set, by adding a decorator in the class definition.

function Test() {
    return (target: object, key: string) => {
        let val = '';
        Object.defineProperty(target, key, {
            get: () => val,
            set: (v: string) => {
                if(/.*\s.*/.test(v)) throw new Error(`Id '${v}' must not contain any whitespace!`)
                val = v;
            },
            enumerable: true
        });
    }
}

class FooClazz {
    @Test()
    id: string;

    constructor(id: string) {
        this.id = id;
    }
}

const test = new FooClazz("myId");

console.log(Object.keys(test)) // -> [];

The setter validator works, but when I try to log out the object or its keys, id does not show up. Even though I set enumerable to true.

What am I doing wrong?


Solution

  • The problem is Test decorator decorates the prototype property of the FooClazz class. But you're trying to get own keys of FooClazz's instance. While id property exists on it's prototype chain:

    console.log(Object.keys(test.__proto__)) // -> ["id"];
    

    Yet another problem is Test decorator runs only once (when decorates FooClazz's prototype) and val variable is 'shared' among the instances (though it may be intentional?).

    const test = new FooClazz("myId");
    const anotherTest = new FooClazz("anotherId");
    
    console.log(test.id);         // -> "anotherId"
    console.log(anotherTest.id);  // -> "anotherId"
    

    You may use a trick with redefining the key on the instance itself on the first set:

    function Test(target: any, key: string) {
        Object.defineProperty(target, key, {
            get: () => '',
            set: function(v: string) {
                var val = '';
                Object.defineProperty(this, key, {
                    get: () => val,
                    set: (v: string) => {
                        if(/.*\s.*/.test(v)) throw new Error(`Id '${v}' must not contain any whitespace!`)
                        val = v;
                    },
                    enumerable: true,
                });
                this[key] = v;
            },
        }); 
    }
    
    class FooClazz {
        @Test
        id: string;
    
        constructor(id: string) {
            this.id = id;
        }
    }
    

    playground link