Strange behavior when working with Typescript decorators and Object.defineProperty

I'm currently working on a project that involves creating a decorator to override a property and define a hidden property. Let's take a look at the following example:

function customDecorator() {
    return (target: any, key: string) => {

        let hiddenKey = '_' + key;

        // Define a hidden property
        Object.defineProperty(target, hiddenKey, {
            value: 0,
            enumerable: false,
            configurable: true,
            writable: true
        });

        // Override property get/set
        return Object.defineProperty(target, key, {
            enumerable: true,
            configurable: true,
            get: () => target[hiddenKey],
            set: (val) => {
                target[hiddenKey] = target[hiddenKey] + 1;
            }
        });
    };
}

class ExampleClass {
    @customDecorator()
    propA = null;
    propB = null;
}

let instance = new ExampleClass();

console.log(Object.keys(instance), instance.propA, instance._propA, instance);

The current output is:

[ 'propB' ] 1 1 A { propB: null }

However, the expected output is:

[ 'propA', 'propB' ] 1 1 A { propA: 1, propB: null }

despite propA being enumerable.

If we replace the get and set with:

get: function () {
    return this[hiddenKey]
},
set: function (val) {
    this[hiddenKey] = this[hiddenKey] + 1;
}

We now get:

[ '_propA', 'propB' ] 1 1 A { _propA: 1, propB: null }

even though _propA has its enumerable set to false.

It seems like there are some unexpected behaviors happening here. I would appreciate an explanation of what is going on and how to achieve the desired outcome in this scenario.

Answer №1

After significant troubleshooting, I was able to discover a workaround for the issue at hand. It appears that Object.defineProperty does not function correctly during decoration time. However, if applied during runtime, the desired behavior is achieved. So, how can you define a property within a decorator but execute it at runtime?

The key lies in overriding the property within the decorator at decoration time (although only the enumerable aspect seems to be affected). By defining the property with an initialization function instead of using getter and setter, the desired functionality is preserved. This function will execute when the property is first accessed (get) or assigned (set). At this point, the keyword this refers to the runtime instance of the object, allowing for proper initialization as intended during decoration time.

Here is the resolved solution:

function f() {
    return (target: any, key: string) => {
        let pKey = `_${key}`;

        let init = function (isGet: boolean) {
            return function (newVal?) {
                /*
                 * This is called at runtime, so "this" is the instance.
                 */

                // Define hidden property
                Object.defineProperty(this, pKey, {value: 0, enumerable: false, configurable: true, writable: true});
                // Define public property
                Object.defineProperty(this, key, {
                    get: () => {
                        return this[pKey];
                    },
                    set: (val) => {
                        this[pKey] = this[pKey] + 1;
                    },
                    enumerable: true,
                    configurable: true
                });

                // Perform original action
                if (isGet) {
                    return this[key]; // get
                } else {
                    this[key] = newVal; // set
                }
            };
        };

        // Override property to let init occur on first get/set
        return Object.defineProperty(target, key, {
            get: init(true),
            set: init(false),
            enumerable: true,
            configurable: true
        });
    };
}

This implementation results in:

[ 'propA', 'propB' ] 1 1 A { propA: [Getter/Setter], propB: null }

The provided solution accommodates default values, ensuring they are assigned after the correct initialization of get/set operations.

Additionally, it appropriately supports enumerable: setting enumerable to true for property pKey yields the following output:

[ '_propA', 'propA', 'propB' ] 1 1 A { _propA: 1, propA: [Getter/Setter], propB: null }

Although not aesthetically pleasing, this solution effectively functions without compromising performance.

Answer №2

Upon reviewing your code, it came to my attention that the property was being defined twice. I have made some modifications to your code for optimization.

class A {
    @dec
    public value: number = 5
}

function dec(target, key) {
    function f(isGet: boolean) {
        return function (newValue?: number) {
            if (!Object.getOwnPropertyDescriptor(this, key)) {
                let value: number;
                const getter = function () {
                    return value
                }

                const setter = function (val) {
                    value = 2 * val
                }
                Object.defineProperty(this, key, {
                    get: getter,
                    set: setter,
                    enumerable: true,
                    configurable: true
                })
            }
            if (isGet) {
                return this[key]
            } else {
                this[key] = newValue
            }
        }
    }

    Object.defineProperty(target, key, {
        get: f(true),
        set: f(false),
        enumerable: false,
        configurable: false
    })
}

const a = new A()
console.log(Object.keys(a))

You can observe the following output in the console:

["value"]

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Puppeteer: implementing wait timeout is crucial to successfully handle Auth0 login process

Recently, I started using puppeteer and encountered some unexpected behavior. It seems that the waitForSelector function does not work properly unless I include a delay before it. Take a look at the following code: const browser = await puppeteer.l ...

Cypress: Importing line in commands.ts is triggering errors

After adding imports to the commands.ts file, running tests results in errors. However, in commands.ts: import 'cypress-localstorage-commands'; /* eslint-disable */ declare namespace Cypress { interface Chainable<Subject = any> { c ...

React TypeScript: The properties of 'X' are not compatible. 'Y' cannot be assigned to 'Z' type

I am currently working on a React-TypeScript application, specifically creating a component for inputting credit card numbers. My goal is to have the FontAwesome icon inside the input update to reflect the brand image as the user enters their credit card n ...

Different ways to showcase a value from the CSS file on the console using console.log

In this guide, you can learn how to create a custom directive in Angular by following this tutorial: Custom Directive Tutorial. The directive should function as intended. Still, I want to see the color value set in the CSS file displayed on the console us ...

What happens when a typed Array in Typescript has an undefined property?

I've encountered an issue with a seemingly simple problem that's causing me quite the headache. The code snippet in question is provided below: interface IFoo{ ReturnFirstBarObject1(): string; FillBarArray(array: Array<Bar>): void; } ...

The Angular Button fails to display when all input conditions are met

I'm currently working on validating a form using this link. The requirement is simple - when an input field is invalid, the `save` button should be disabled. Conversely, when all input fields are valid, the `SAVE` button should be enabled/shown. The & ...

Modifying the name of a key in ng-multiselect-dropdown

this is the example data I am working with id: 5 isAchievementEnabled: false isTargetFormEnabled: true name: "NFSM - Pulse" odiyaName: "Pulse or" when using ng-multiselect-dropdown, it currently displays the "name" key. However, I want ...

How can one properly conduct a health check on a Twilio connection using TypeScript?

How can I create an endpoint in TypeScript to verify if the Twilio connection is properly established? What would be the proper method to perform this check? Below is a snippet of my current code: private twilioClient: Twilio; ... async checkTwilio() { ...

Can you guide me on how to record a value in Pulumi?

According to Pulumi's guidance on inputs and outputs, I am trying to use console.log() to output a string value. console.log( `>>> masterUsername`, rdsCluster.masterUsername.apply((v) => `swag${v}swag`) ); This code snippet returns: & ...

Navigating with Angular: Transmitting dynamic URL parameters to components

I currently have the following routes defined: const routes: Routes = [ { path: ':product/new', children: [{ path: 'std/:country', component: SignUpComponent, data: { ...

Having difficulty casting the parameter type from Array.find() in TypeScript

In my codebase, I am dealing with the OrganisationInterface type: export declare interface OrganisationInterface { documents?: { [documentType: OrganisationDocumentTypesList]: { // enum id: string; name: string; ...

Using Typescript types or a linter can help avoid mistakenly rendering the raw function in JSX instead of its executed return value

Seeking a resolution to prevent the recurring mistake I often make, as shown below: export function scratch_1 (props: scratch_1Props): ReactElement | null { function renderA (): string { return "A"; } function renderB (): string { ...

TypeScript enum type encompassing all potential values

One thing I have learned is that keyof typeof <enum> will give us a type containing all the possible keys of an enum. For example, if we have enum Season{ WINTER = 'winter', SPRING = 'spring', SUMMER = 'summer', AUT ...

"What is the methodology for specifying generics in a TypeScript FC component?"

How do you specify the type to pass to an Interface Props generic? (The Cat must be of type FC) interface CatProps<T> { value: T } const Cat: FC<CatProps<T>> = () => { return <h1>Hello World!</h1> } const cat = <Ca ...

What is the process for utilizing a Typescript Unit Test to test Typescript code within Visual Studio?

Currently, I am facing an issue while writing a unit test in Typescript to check a Typescript class. The problem arises when the test is executed as it is unable to recognize the class. To provide some context, my setup includes Typescript (1.4) with Node ...

Basic exam but located in a place that is not valid

Here is a test I am working on: // import {by, element, browser} from "protractor"; describe('intro', () => { beforeEach(() => { browser.get(''); }); it('should have multiple pages', () => { let buttonOn ...

Converting JavaScript object data to x-www-form-urlencoded: A step-by-step guide

I am trying to convert a JavaScript object into x-www-form-urlencoded. Is there a way to achieve this using Angular 2? export class Compentency { competencies : number[]; } postData() { let array = [1, 2, 3]; this.comp.competencies ...

When converting a PDF to a PNG, the precious data often disappears in the process

I am currently facing a problem with the conversion of PDF to PNG images for my application. I am utilizing the pdfjs-dist library and NodeCanvasFactory functionality, but encountering data loss post-conversion. class NodeCanvasFactory { create(w, h) { ...

Can we limit the return type of arrow function parameters in TypeScript?

Within my typescript code, there is a function that takes in two parameters: a configuration object and a function: function executeMaybe<Input, Output> ( config: { percent: number }, fn: (i: Input) => Output ): (i: Input) => Output | &apos ...

When running on localhost, IE11 only shows a white screen while the other browsers function properly

I have recently completed a web-based project and successfully deployed it. The project is running on port 8080. Upon testing in Chrome, Safari, and Firefox, the project functions without any issues, and no errors are displayed in the console. However, wh ...