Tips for correctly decorating constructors in TypeScript

When a class is wrapped with a decorator, the superclasses lose access to that classes' properties. But why does this happen?

I've got some code that demonstrates the issue:

  1. First, a decorator is created which replaces the constructor of a class with a new one that should essentially perform the same task.
  2. Next, a base class with a property is defined.
  3. The base class is then wrapped with the decorating function.
  4. A subclass is created that extends the base class.
  5. Now when trying to access the property on the extended class, it fails to retrieve the value.

Take a look at the code snippet below:

function wrap(target: any) {
  // the new constructor
  var f: any = function (...args) {
      return new target();
  }

  f.prototype = target.prototype;
  return f;
}

@wrap
class Base {
    prop: number = 5;
}

class Extended extends Base {
    constructor() {
        super()
    }
}

var a = new Extended()
console.log(new Extended().prop) // I'm expecting 5 here, but I get undefined.

It seems like there's a subtlety related to either prototypes in general or the specific way TypeScript handles them that I haven't fully grasped yet.

Answer №1

This updated method leverages the latest TS version (3.2.4). It also utilizes the decorator factory design pattern, allowing for attributes to be passed in:

function CustomDecorator(attr: any) {
  return function _CustomDecorator<T extends {new(...args: any[]): {}}>(constr: T){
    return class extends constr {
      constructor(...args: any[]) {
        super(...args)
        console.log('Performing additional tasks after the original constructor!')
        console.log('Here is the specified attribute:', attr.attrName)
      }
    }
  }
}

For more information, visit: https://www.typescriptlang.org/docs/handbook/decorators.html#class-decorators

Answer №2

This code snippet is what I use and it works perfectly:

function logClass(target: any) {
  // store a reference to the original constructor
  var original = target;

  // define new constructor behavior
  var f : any = function (...args) {
    console.log("New: " + original.name); 
    return new original(...args); // as per comments
  }

  // replicate prototype for instanceof operator compatibility
  f.prototype = original.prototype;

  // provide new constructor (overriding the original)
  return f;
}

@logClass
class Base {
    prop: number = 5;
}

class Extended extends Base {
    constructor() {
        super()
    }
}

var b = new Base()
console.log(b.prop)

var a = new Extended()
console.log(a.prop)

Answer №3

One way to override the constructor using ES2015 Proxy:

function wrapConstructor(target: any) {
  return new Proxy(target, {
    construct(clz, args) {
      console.log(`Creating instance of ${target.name}`);
      return Reflect.construct(clz, args);
    }
  });
}

@wrapConstructor
class BaseClass {
  property: number = 5;
}

class ExtendedClass extends BaseClass {
  constructor() {
    super()
  }
}

var instanceA = new ExtendedClass()
console.log(new ExtendedClass().property);

For a live demo, you can try running this code on StackBlitz

Answer №4

Previous responses have mentioned that the code doesn't function properly.
In reality, it does work but not on jsFiddle...
This issue stems from the code generation in jsFiddle (potentially due to using an outdated version of TypeScript).
The provided code is compatible with TypeScript 2.7.2 (executed with Node).

Essentially, this is the same code as pablorsk's solution (sans the necessity to return the instance), with detailed types added for stricter TSLint compliance...

function logClass<T extends { new(...args: any[]): {} }>(): any {
    type Ctor = new (...args: any[]) => T;
    return (target: T): Ctor => {
        // Preserve the original constructor
        const Original = target;

        // Adjusted constructor behavior
        let decoratedConstructor: any = function (...args: any[]): void {
            console.log("Before construction:", Original);
            Original.apply(this, args);
            console.log("After construction");
        };

        // Maintain prototype for instanceof operator functionality
        decoratedConstructor.prototype = Original.prototype;
        // Duplicate static members as well
        Object.keys(Original).forEach((name: string) => { decoratedConstructor[name] = (<any>Original)[name]; });

        // Return updated constructor replacing the original one
        return decoratedConstructor;
    };
}

@logClass()
class Base {
    prop = 5;
    constructor(value: number) {
        console.log("Base constructor", value);
        this.prop *= value;
    }
    foo() { console.log("Foo", this.prop); }
    static s() { console.log("Static s"); }
}

class Extended extends Base {
    constructor(init: number) {
        super(init);
        console.log("Extended constructor", init);
    }
    bar() { console.log("Bar", this.prop); }
}

const b = new Base(2);
console.log("Base", b instanceof Base);
b.foo();
Base.s();

const e = new Extended(5);
console.log("Extended", e instanceof Base, e instanceof Extended);
e.bar();

[UPDATE] Included a line for copying static members to prevent errors when invoking the static method within the decorated class.

Answer №5

If you enjoy working with decorators in TypeScript to run code before and after the constructor function:

function DecorateConstructor() {
    return function(target: any) {
        // storing reference to original constructor
        var original = target;

        // defining new constructor behavior
        var f: any = function (...args) {
            console.log('DecorateConstructor: before class constructor', original.name);
            let instance = original.apply(this, args)
            console.log('DecorateConstructor: after class constructor', original.name);
            return instance;
        }

        // maintaining compatibility with instanceof operator
        f.prototype = original.prototype;

        // returning new constructor (overriding original)
        return f;
    };
}
@DecorateConstructor()
export class ExampleClass {
    public constructor() {
        console.info('Executing ExampleClass constructor...');
    }
}

let exampleObject = new ExampleClass();

/*
CONSOLE OUTPUT:
DecorateConstructor: before class constructor ExampleClass
Executing ExampleClass constructor...
DecorateConstructor: after class constructor ExampleClass
*/

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

What is the best way to relocate the styles folder to the src folder while using nextjs, typescript, and tailwind

I am currently working with Next.js using TypeScript and Tailwind CSS. My goal is to relocate the styles folder into the src folder. I have already updated the baseUrl in my tsconfig.json file to point to the src directory, but I encountered the following ...

Creating a conditional interface based on props in TypeScript: A step-by-step guide

What is the most effective way to implement conditional props in a component that can be either a view or a button based on certain props? Let's take a look at an example called CountdownButtonI: class CountDownButton extends Component<CountdownBut ...

Angular 2 Login Component Featuring Customizable Templates

Currently, I have set up an AppModule with a variety of components, including the AppComponent which serves as the template component with the router-outlet directive. I am looking to create an AuthModule that includes its own template AuthComponent situa ...

"Using an indexer in TypeScript allows for direct access to object properties by simply specifying the key within

I have a requirement to access an object property using a string as the key interface MyObject { prop1: string; prop2: string; prop3: string; prop4: string; prop5: string; } let initialValues: MyObject; //I initialize some properties initialVa ...

The router's handler function sends back a collection of objects, but for some reason, the client is not receiving them in JSON format even though the response

I am currently developing an Express.js project using Typescript. In my project, I have defined an enum and an interface as follows: export enum ProductCategory { ELECTRONICS = 'electronics', CLOTHING = 'clothing', TOYS = & ...

What is the best way to click on a particular button without activating every button on the page?

Struggling to create buttons labeled Add and Remove, as all the other buttons get triggered when I click on one. Here's the code snippet in question: function MyFruits() { const fruitsArray = [ 'banana', 'banana', & ...

Set the default value for a form control in a select dropdown using Angular

I've been struggling to figure out how to mark an option as selected in my select element, but I haven't had any luck. I've tried multiple solutions from the internet, but none of them seem to be working for me. Does anyone out there have ...

`How can I effectively test a React.js page utilizing both Context and useEffect?`

I'm struggling with testing a page that uses Context and useEffect with Jest and Testing-library, can you offer any assistance? REPOSITORY: https://github.com/jefferson1104/padawan Context File: src/context/personContext.tsx import { createContext, ...

Creating an Observable from static data in Angular that resembles an HTTP request

I have a service with the following method: export class TestModelService { public testModel: TestModel; constructor( @Inject(Http) public http: Http) { } public fetchModel(uuid: string = undefined): Observable<string> { i ...

Angular offers pre-determined values that cannot be altered, known as "

I am currently learning Angular and TypeScript, and I came across a task where I need to create an object or something similar that allows me to define a readable but not editable attribute. In Java, I would have achieved this by doing the following: publ ...

Issue with webpack dev server not correctly generating output files to be included in index.html

Struggling to configure webpack and react with typescript without the complexity of CRA. The dev server isn't outputting files to index.html for viewing in the browser. I want to maintain a clean and simple structure, avoiding the multiple js scripts ...

Alternative for using useRouteMatch to retrieve parameters

I'm currently refactoring this code and struggling to find a suitable replacement for this section. This is due to the react-router-dom v6 no longer having the useRouteMatch feature, which I relied on to extract parameters from the URL: import React, ...

Implementing computed properties: A guide to incorporating type setting

I currently have two separate interfaces defined for Person and Dog. interface Person { name: string; weight: number; } interface Dog { name: string; mass: number } const specificAttribute = isDog ? 'mass' : 'weight'; ...

How can Angular developers properly implement token refreshing in their applications?

Recently, I've been struggling with implementing a logic in my code. I have a specific requirement: Whenever there is a signed request (signed - means it has a JWT token for authenticated users) made to the API backend, the API backend may respond w ...

Dealing with circular dependencies in NestJS by using ForwardRef

Hey everyone, I've been running into a circular dependency issue with NestJS. I tried using the forwardref method, but it hasn't resolved the problem for me. // AuthModule @Module({ imports: [ forwardRef(() => UserModule), JwtModule ...

Combining 2 Observables in nestjs Interceptor: A Step-by-Step Guide

I am currently working on a NestJS interceptor to geolocate an address that is being sent through a REST API. Here is the code snippet: export class PointsInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): O ...

Issues with implementing Dark mode in TailwindCSS with Nuxt.js

After spending a couple of days on this, I'm still struggling to get the dark mode working with Tailwind CSS in Nuxt.js. It seems like there might be an issue with the CSS setup rather than the TypeScript side, especially since I have a toggle that sw ...

Angular2 and Typescript paired with Visual Studio 2013

Currently, I am utilizing the angular2 QUICKSTART and encountering an issue where Visual Studio fails to recognize Angular2 with typescript import Modules. However, everything else seems to be functioning correctly: https://i.stack.imgur.com/0s46Y.jpg Th ...

If you're trying to work with this file type, you might require a suitable loader. Make sure you

Attempting to utilize Typescript typings for the Youtube Data API found at: https://github.com/Bolisov/typings-gapi/tree/master/gapi.client.youtube-v3 Within the Ionic framework, an error is encountered after running 'Ionic Serve' with the follo ...

The variable <variable> is not meeting the 'never' constraint. Error code: ts(2344)

Currently, I am attempting to utilize Supabase alongside TypeScript. However, I encounter an error when trying to use functions like insert(), update(), upsert(), etc. Specifically, the issue arises when declaring the object I want to declare: "Type & ...