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

Unlock the Power of Typography in React with Material UI Styled Components

Exploring the world of material UI is a new experience for me. I am currently in the process of creating a styled component using Typography. Below is what I have attempted so far: import styled from 'styled-components'; import { FormGroup, ...

Are the missing attributes the type of properties that are absent?

I have a pair of interfaces: meal-component.ts: export interface MealComponent { componentId: string; componentQuantity: number; } meal.ts: import { MealComponent } from 'src/app/interfaces/meal-component'; export interface Meal { ...

Issue: Formcontrolname attribute is undefined causing TypeError when trying to retrieve 'get' property.Remember to define formcontrolname attribute to

Having trouble creating a form at the moment and keep encountering this error: 'ERROR TypeError: Cannot read property 'get' of undefined' Even after trying various solutions like using formControlName in brackets or accessing the va ...

Unable to create resource in nestjs due to typeScript compatibility issue

Encountered an Error: TypeError: Cannot access 'properties' property of undefined Failed to execute command: node @nestjs/schematics:resource --name=post --no-dry-run --language="ts" --sourceRoot="src" --spec Attempts made ...

Tips for accessing the app instance within a module in Nest.js

Currently, I am involved in a project that relies on multiple Nest repositories, approximately 4 in total. Each repository must integrate logging functionalities to monitor various events such as: Server lifecycle events Uncaught errors HTTP requests/resp ...

Creating an Http interceptor in Ionic 3 and Angular 4 to display a loading indicator for every API request

One of my current challenges involves creating a custom HTTP interceptor to manage loading and other additional functions efficiently. Manually handling loading for each request has led to a considerable increase in code. The issue at hand: The loader is ...

Handling errors in nested asynchronous functions in an express.js environment

I am currently developing a microservice that sends messages to users for phone number verification. My focus is on the part of the microservice where sending a message with the correct verification code will trigger the addition of the user's phone n ...

Using the concat operator along with the if statement in Angular to make sequential requests based on certain conditions

Managing multiple HTTP requests in a specific order is crucial for my event. To achieve this, I am utilizing the concat operator from rxjs. For instance, upon receiving data from the first request, I update local variables accordingly and proceed to the ne ...

Is it possible that React.createElement does not accept objects as valid react children?

I am working on a simple text component: import * as React from 'react' interface IProps { level: 't1' | 't2' | 't3', size: 's' | 'm' | 'l' | 'xl' | 'xxl', sub ...

Styling Form validation with Ant Design

Can a className prop be included in the Form.Item validation? <Form.Item name="username" rules={[ { required: true, message: '...' }, className="customValidation" //<- attempting to add a className but it is not fu ...

Issue Establishing Connection Between Skill and Virtual Assistant Via Botskills Connect

Encountering errors while connecting a sample skill to a virtual assistant. Both are in typescript and function individually, but when using botskills connect, the following errors occur: Initially, ran botskills connect with the --localManifest parameter ...

What is the best way to extract a nested array of objects and merge them into the main array?

I've been working on a feature that involves grouping and ungrouping items. A few days ago, I posted this question: How can I group specific items within an object in the same array and delete them from the core array? where some helpful individuals ...

Leverage the power of InfiniteSWR in your project by integrating it seamlessly with

If you're looking for a comprehensive example of how to integrate useSWR hooks with axios and typescript, check out the official repository here. import useSWR, { SWRConfiguration, SWRResponse } from 'swr' import axios, { AxiosRequestConfig, ...

Automate the process of replacing imports in Jest automatically

Currently, I am in the process of setting up a testbench for a more intricate application. One challenge we are facing is that our app needs to call backend code which must be mocked to ensure the testbench runs efficiently. To address this issue, we utili ...

Error TS2339 occurs when attempting to migrate to TypeScript due to the absence of the 'PropTypes' property on the 'React' type

Currently in the process of converting a javascript/react project to a typescript/react/redux project. Encountering an issue with this particular file: import React from 'react'; import GoldenLayout from 'golden-layout'; import {Provi ...

Exploring the functionality of generic components in React Native when using TypeScript

As an illustration, consider export class FlatList<ItemT> extends React.Component<FlatListProps<ItemT>> which incorporates the generic type ItemT. How can I utilize it in a .tsx code? When not parametrized, it appears like this: <Flat ...

Having trouble injecting $resource into my Jasmine unit test

I've encountered an issue while trying to test my self-written service that utilizes $resource. I'm facing difficulties when trying to inject $resource into my test spec. My setup includes Typescript with AngularJS 1.6.x, and here is a snippet o ...

Trouble with querying NG elements using "queryAll(By.css)" in Angular and Jasmin unit testing

I've encountered an unusual problem that needs to be resolved for me to successfully complete a unit test for a project I'm currently engaged in. Here is what my unit test currently looks like: it('should display the navbar list', ...

Ionic Vue is throwing an error indicating that a property is not found on the type '{ $route(currentRoute: any): void; }'

Currently, I am developing an Ionic WebApp using Vue and TypeScript. My current task involves retrieving the current id parsed by the route. To achieve this, I have been working on a watcher: export default { data() { return { ...

Automatically dismiss modal upon submission

In the process of implementing a CRUD operation, I am trying to automatically close the modal upon submission. Using data-dismiss on the submit button only closes the modal without executing the functionality. What I need is for the functionality to execut ...