Enhance your TypeScript code using decorators with inheritance

Exploring the realm of Typescript decorators has led me to discover their intriguing behavior when combined with class inheritance.

Consider the following scenario:

class A {
    @f()
    propA;
}

class B extends A {
    @f()
    propB;
}

class C extends A {
    @f()
    propC;
}

function f() {
    return (target, key) => {
        if (!target.test) target.test = [];
        target.test.push(key);
    };
}

let b = new B();
let c = new C();

console.log(b['test'], c['test']);

The current output is:

[ 'propA', 'propB', 'propC' ] [ 'propA', 'propB', 'propC' ]

However, the expected output would be:

[ 'propA', 'propB' ] [ 'propA', 'propC' ]

This suggests that target.test is shared among classes A, B, and C. Here's my analysis:

  1. When instantiating new B(), it triggers initialization of class A first, invoking the evaluation of f for A. As target.test is undefined, it gets initialized.
  2. Subsequently, f is called for class B after extending from A. At this point, target.test in class B references the same instance as class A, resulting in the accumulation of properties propA and propB. So far, so good.
  3. A similar process occurs with class C inheriting from A. Despite my anticipation of a distinct test object for C, the shared log proves otherwise.

I am seeking insights on why this behavior persists and how I can modify f to grant separate test properties for A and B. Could this involve creating an "instance specific" decorator?

Answer №1

After dedicating some time to experimenting and researching online, I managed to create a functional version. Although I'm unsure of the reasoning behind its success, I hope you can overlook the absence of an explanation.

The crucial point lies in utilizing

Object.getOwnPropertyDescriptor(target, 'test') == null
instead of !target.test for checking the existence of the test property.

If you implement:

function f() {
    return (target, key) => {
        if (Object.getOwnPropertyDescriptor(target, 'test') == null) target.test = [];
        target.test.push(key);
    };
}

the following will be displayed on the console:

[ 'propB' ] [ 'propC' ]

This outcome is close to my desired result. Nonetheless, the array is now unique to each instance. Consequently, 'propA' is absent from the array as it is defined in A. Therefore, we must access the parent target and retrieve the property from there. After some trial and error, I discovered that this can be achieved using Object.getPrototypeOf(target).

The finalized solution consists of:

function f() {
    return (target, key) => {
        if (Object.getOwnPropertyDescriptor(target, 'test') == null) target.test = [];
        target.test.push(key);

        /*
         * Now that the target is specific, include properties defined in the parent.
         */
        let parentTarget = Object.getPrototypeOf(target);
        let parentData = parentTarget.test;
        if (parentData) {
            parentData.forEach(val => {
                if (target.test.find(v => v == val) == null) target.test.push(val);
            });
        }
    };
}

The resulting output is:

[ 'propB', 'propA' ] [ 'propC', 'propA' ]

I would appreciate any insight from those who comprehend why this approach succeeds where the previous one falls short.

Answer №2

The reason for the behavior in your code is due to the fact that your decorated field is an instance member, and the target you receive is the prototype of the class. As the execution begins, the class A is loaded first since it is the parent class. Consequently, the test array is set on the prototype of class A, which is shared by all Child Classes B/C. This explains why you see 3 elements in the test array.

Instead of utilizing getOwnPropertyDescriptor(), one alternative approach is to store the metadata on the target.constructor itself, which represents the class. Each class will then have its own metadata stored, making it easier to gather all decorated fields by searching up the prototype chain (with assistance from the standard relect-metadata).

function f() {
  return (target, key) => {
    if (!Reflect.hasOwnMetadata('MySpecialKey', target.constructor)) {
      // add field list to the class.
      Reflect.defineMetadata('MySpecialKey', [], target.constructor);
    }
    Reflect.getOwnMetadata('MySpecialKey', target.constructor).push(key);
  };
}

/**
 * @param clz the class/constructor
 * @returns the fields decorated with @f throughout the prototype chain.
 */
static getAllFields(clz: Record<string, any>): string[] {
if(!clz) return [];
const fields: string[] | undefined = Reflect.getMetadata('MySpecialKey', clz);
// retrieve `__proto__` and (recursively) all parent classes
const rs = new Set([...(fields || []), ...this.getAllFields(Object.getPrototypeOf(clz))]);
return Array.from(rs);
}


Alternatively, you can consider following the class-validator approach which involves a global metadata storage containing decorator-related information. During the logic execution, check if the target constructor is an instance of the registered target. If so, include the field in the validation process.

Answer №3

One possible explanation is that when class B is created, the prototype of A is duplicated along with all its unique properties (as references).

I decided to implement a slightly modified solution which seems to address the issue of duplicates more effectively, especially if class C does not contain any decorators.

I am still uncertain if this approach is the most efficient way to handle such scenarios:


    function foo(target, key) {
        let
            ctor = target.constructor;

        if (!Object.getOwnPropertyDescriptor(ctor, "props")) {
            if (ctor.props)
                ctor.props = [...ctor.props];
            else
                ctor.props = [];
        }

        ctor.props.push(key);
    }

    abstract class A {
        @foo
        propA = 0;
    }

    class B extends A {
        @foo
        propB = 0;
    }

    class C extends A {
        @foo
        propC = 0;
    }

Answer №4

@user5365075 I encountered a similar issue while using method decorators, but your solution proved to be effective.

Below is an example of a decorator I implemented in one of my projects, utilizing an object instead of an array:

export function property(options) {
    return (target, name) => {
        // This workaround addresses a bug discussed here:
        // https://stackoverflow.com/questions/43912168/typescript-decorators-with-inheritance

        if (!Object.getOwnPropertyDescriptor(target, '_sqtMetadata')) {
            target._sqtMetadata = {}
        }

        if (target._sqtMetadata.properties) {
            target._sqtMetadata.properties[name] = options.type
        } else {
            target._sqtMetadata.properties = { [name]: options.type }
        }

        const parentTarget = Object.getPrototypeOf(target)
        const parentData = parentTarget._sqtMetadata

        if (parentData) {
            if (parentData.properties) {
                Object.keys(parentData.properties).forEach((key) => {
                    if (!target._sqtMetadata.properties[key]) {
                        target._sqtMetadata.properties[key] = parentData.properties[key]
                    }
                })
            }
        }
    }
}

I can attest that the behavior described also applies to class decorators.

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

TypeScript fails to acknowledge an exported enum

Currently utilizing TypeScript version 2.5.3 along with Angular 5. I have an enum defined in a separate file as follows: export enum eUserType { Driver = 1, Passenger = 2, User = 3 } To import and use it in another ts file, I do the following: i ...

Exploring the Behavior of Typescript Modules

When working with the module foo, calling bar.factoryMethod('Blue') will result in an instance of WidgetBlue. module foo { export class bar { factoryMethod(classname: string): WidgetBase { return new foo["Widget" + classname](); ...

js TouchEvent: When performing a pinch gesture with two fingers and lifting one of them up, how can you determine which finger was lifted?

I am currently working on a challenging touching gesture and have encountered the following issue: let cachedStartTouches: TouchList; let cachedMoveTouches: TouchList; function onStart(ev: TouchEvent) { // length equals 2 when two fingers pinch start ...

The attribute 'data' is not found in the type 'IntrinsicAttributes & IProps'. Error code: ts(2322)

I encountered the following issue: Error: Type '{ data: never; }' is not compatible with type 'IntrinsicAttributes & IProps'. The property 'data' does not exist on the type 'IntrinsicAttributes & IProps'. import { ...

The Gulp task is stuck in an endless cycle

I've set up a gulp task to copy all HTML files from a source folder to a destination folder. HTML Gulp Task var gulp = require('gulp'); module.exports = function() { return gulp.src('./client2/angularts/**/*.html') .pipe( ...

I need to compile a comprehensive inventory of all the publicly accessible attributes belonging to a Class/Interface

When working with TypeScript, one of the advantages is defining classes and their public properties. Is there a method to list all the public properties associated with a particular class? class Car { model: string; } let car:Car = new Car(); Object. ...

The noUnusedLocal rule in the Typescript tsconfig is not being followed as expected

I am currently working on a project that utilizes typescript 3.6.3. Within my main directory, I have a tsconfig.json file with the setting noUnusedLocals: true: { "compilerOptions": { "noUnusedLocals": true, "noUnusedParameters": true, }, ...

Is it possible to leverage ES6 modules within a Node.js application while also debugging it using Visual Studio?

Trying to create a basic node.js module test project using ES6 in Visual Studio 2015 has resulted in build errors, preventing me from running or debugging the application. Could it be that I arrived at the party too soon? I attempted opening and building ...

Obtain the value of a template variable in Angular 2

I am seeking information on how to access the values of selected items in templates. Specifically, I want to understand how to retrieve the selected value of IPMIDisplayTime and IPMIDisplayTime within the template for later use. import {ViewChild, Elem ...

What is the significance of utilizing an empty value `[]` for a typed array interface instead of using an empty `{}` for a typed object interface?

Why can I initialize friends below as an empty array [], but not do the same for session with an empty object {}? Is there a way to use the empty object without needing to make all keys optional in the interface? const initialState: { friends: Array< ...

Error encountered in Angular NGRX while accessing the store: Trying to read property 'map' of an undefined variable

I have integrated NGRX effects into my Angular application and encountered the following error. I'm uncertain if I am using the selector correctly in my component to query the store? core.js:6162 ERROR TypeError: Cannot read property 'map' o ...

Error message: Typescript and Styled-Component conflict: GlobalStylesProps does not share any properties with type { theme?: DefaultTheme | undefined; }

I've encountered an issue while passing props inside GlobalStyles in the preview.js file of my storybook. The props successfully remove the background from the default theme, but I'm receiving an error from Typescript: The error message states " ...

When utilizing makeStyles in @mui, an error may occur stating that property '' does not exist on type 'string'

I am stuck with the code below: const useStyles = makeStyles(() => ({ dialog: { root: { position: 'absolute' }, backdrop: { position: 'absolute' }, paperScrollPaper: { overflow: 'visib ...

The module 'node:fs' could not be located. Stack required:

I've been developing a Teams app with my tab in React and TypeScript. (In case you're unfamiliar, the tab can be seen as an individual React app) Currently, I'm setting up linting using ESLint and Prettier. I have successfully run the scri ...

Angular 6 Checkbox Selector - Filtering Made Easy

How can I filter a list of JSON objects (Products) by the 'category' variable using checkboxes? An example product object is shown below: { 'bikeId': 6, 'bikeName': 'Kids blue bike', 'bikeCode': ...

Tips for sending a function as a value within React Context while employing Typescript

I've been working on incorporating createContext into my TypeScript application, but I'm having trouble setting up all the values I want to pass to my provider: My goal is to pass a set of values through my context: Initially, I've defined ...

A step-by-step guide to customizing the Material UI Chips delete SVG icon to appear in white color through

Using a Material UI component, I added a custom class to my chip. Attached is a screenshot showing what I mean. Currently, I am attempting to change the color of the cross button to white. After inspecting the element, I discovered that it is an SVG ico ...

There was an error in parsing the module: an unexpected token was encountered during the rendering

Recently, I've been working on configuring React with Typescript (for type checking), Babel for code transpilation, Jest for testing, ESLint for code checking, and a few other tools. You can find all the necessary files in the repository linked below. ...

What is the best approach for managing Create/Edit pages in Next.js - should I fetch the product data in ServerSideProps or directly in the component?

Currently, I am working on a form that allows users to create a product. This form is equipped with react-hook-form to efficiently manage all the inputs. I am considering reusing this form for the Edit page since it shares the same fields, but the data wil ...

What is the best way to incorporate a WYSIWYG Text Area into a TypeScript/Angular2/Bootstrap project?

Does anyone know of a WYSIWYG text editor for TypeScript that is free to use? I've been looking tirelessly but haven't found one that meets my needs. Any recommendations or links would be greatly appreciated. Thank you in advance! ...