Sneaky decorator design pattern

I'm struggling to comprehend the code snippet provided below:

  export class Record{

  };

  export class RecordMissingExtendsError{
      constructor(r:any){

      }
  }

  export function Model() {
    return <T extends { new(...args: any[]): {} }>(ctr: T) => {
        if (!(ctr.prototype instanceof Record)) {
            throw new RecordMissingExtendsError(ctr);
        }

        return (class extends ctr {
            constructor(...args: any[]) {
                const [data] = args;
                if (data instanceof ctr) {
                    return data;
                }
                super(...args);
                (this as any)._completeInitialization();
            }
        });
    };
}

Trying to decipher the above code, I've grasped the following:

The Model function returns a type T, and it looks like

T extends { new(...args: any[]): {} }

What is the meaning behind

T extends { new(...args: any[]): {}
? Is T retaining existing properties while also incorporating additional features?

Furthermore, could you clarify the function's return type? Are we appending an extra constructor to T?

(class extends ctr {
            constructor(...args: any[]) {
                const [data] = args;
                if (data instanceof ctr) {
                    return data;
                }
                super(...args);
                (this as any)._completedInitialization();
            }
        });

Answer №1

T extends { new(...args: any[]): {} }
specifies that T must be a constructor function, essentially a class. The specific arguments and return type of the constructor are not important (meaning T can have any number of arguments and may return any type that extends {}, essentially any object type).

When directly invoked, T represents the class itself. This typing approach for decorators is similar to mixins (read more about it in this link).

The result of invoking the function will be a new class that inherits from the decorated class. Instead of adding a constructor, the original one is replaced with a new constructor that calls the original using super.

In this context, the use of generics seems excessive. They are beneficial for mixins as they pass the original class from input parameter to output parameter and add members to the type. However, decorators cannot alter the type structure, so there is no need for forwarding. Additionally, rather than having the constructor return {}, it could be typed to return Record for runtime and compile-time checks:

export class Record{
    protected _completeInitialization(): void {}
};

export function Model() {
  return (ctr: new (...a: any[]) => Record ) => {
      if (!(ctr.prototype instanceof Record)) {
          throw new RecordMissingExtendsError(ctr);
      }

      return (class extends ctr {
          constructor(...args: any[]) {
              const [data] = args;
              if (data instanceof ctr) {
                  return data;
              }
              super(...args);
              this._completeInitialization(); // no assertion since constructor returns a record
          }
      });
  };
}

@Model()
class MyRecord extends Record { }

@Model()// compile time error, we don't extend Record
class MyRecord2  { }

Answer №2

Type Constraint

T must be a type that extends { new(...args: any[]): {} }

Here, the parameter T is restricted to only accept types that are extensions of { new(...args: any[]): {} }. To clarify the formatting, the type should look like this:

{
    new(...args: any[]): {}
}

This definition describes what's called a newable, which is essentially a function object that can only be invoked using the keyword new. For example:

let A: { new(): any; };
A(); // not allowed
new A(); // allowed

let B: { new(foo: string): any; };
B(); // not allowed
new B(); // not allowed, missing parameter
new B('bar'); // allowed

The notation ...args: any[] represents a rest parameter declaration, and the return type {} indicates that an object must be returned, which TypeScript assumes has no properties at all.

Anonymous Class in Return

Regarding the return type: As the Model decorator function is a class decorator, it can actually return a class itself. If such a class is returned, it will be used instead of the original decorated class.

If a class decorator returns a value, it will substitute the class declaration with the provided constructor function.

from the TS handbook

For instance:

// `ctr` often stands for "constructor"
function Decorate(ctr: Function) {
    return class {
        constructor() {
            super();
            console.log('decorated');
        }
    };
}

@Decorate
class A {
    constructor() {
        console.log('A');
    }
}

new A(); // output will be: "A", then "decorated"

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

Discovering Type Definitions in Nuxt.js Project Without Manual Imports in VSCode: A Step-by-Step Guide

Having issues with VSCode not recognizing type definitions automatically in a Nuxt.js project with TypeScript. I'm looking to avoid manually importing types in every file. Here's my setup and the problem I'm facing: Configuration My tsconfi ...

Transitioning from Angular's promises to Observables (RxJS) for making repetitive API calls to a single endpoint

I am facing an issue while working with Angular and I am seeking a solution using Observables instead of Promises (async/await) which I am currently using. The API endpoint allows sorting and pagination by passing parameters like pageSize and page to fetc ...

PrismaClient is currently incompatible with this browser environment and has been optimized for use in an unknown browser when performing updates

While attempting to update a single field in my database using server-actions and tanstackQuery, I encountered the following error message: Error: PrismaClient is unable to run in this browser environment, or has been bundled for the browser (running in ...

Ways to ensure TypeScript shows an error when trying to access an array index

interface ApiResponse{ data: Student[]; } interface Student { name: string; } Imagine a scenario where we receive an API response, and I am confident that it will contain the data field. However, there is a possibility that the data could be an empty ...

How can we incorporate methods using TypeScript?

I'm currently diving into TypeScript and encountering some challenges when trying to incorporate new methods into the DOM or other pre-existing objects. For instance, I'm attempting to implement a method that can be utilized to display colored te ...

React/Typescript: The object might be null

I am currently converting a React component to TypeScript. The component is making an API call, and although I believe my interfaces are correctly set up, I seem to be passing the types incorrectly. How can I resolve the two errors in the Parent componen ...

Tips on using Visual Studio Code to troubleshoot Angular 4 unit tests

I am working on an Angular 4 project with Material design in Visual Studio Code. The setup is done using angular/cli. Currently, I have been writing unit tests using Karma and Jasmine. However, when trying to debug the tests by setting breakpoints, it doe ...

Angular 5 Tutorial: Defining the "of" Method in HTTP Services

Currently, I'm studying the Angular 5 HTTP tutorial. Everything was going smoothly until I encountered a strange issue in my Ionic project where it started throwing an error stating that "of is not defined". /** * Handle Http operation that failed. * ...

Angular 2: Transforming File into Byte Array

Is there a preferred method in Angular2 for converting an input file (such as an image) into a byte array? Some suggest converting the image to a byte array and then sending it to a Web API, while others recommend sending the File "object" to the API for ...

Ensuring that an object containing optional values meets the condition of having at least one property using Zod validation

When using the Zod library in TypeScript to validate an object with optional properties, it is essential for me to ensure that the object contains at least one property. My goal is to validate the object's structure and confirm that it adheres to a sp ...

What is the syntax for creating a link tag with interpolation in Angular 2 / Ionic 2?

As I work on developing an app using Ionic 2/Angular 2, I have encountered a challenge that I am struggling to overcome. Let me provide some context: I am retrieving multiple strings from a webservice, and some of these strings contain links. Here is an e ...

What is the method for confirming whether an emit has been defined?

I have a button that can handle both short and long press events. Before transitioning to emitters, I relied on function callbacks due to my background in React Native. The typings I used were: export type UsePressableEmits = | { (e: "press& ...

The database migration encounters an issue: The module 'typeorm' cannot be located

When I run the following commands: ❯ node --version v16.19.0 ❯ yarn --version 3.5.0 I am attempting to launch this project: https://github.com/felipebelinassi/typescript-graphql-boilerplate However, when I execute: yarn db:migrate which runs the c ...

Help! I keep getting the NullInjectorError in the console saying there is no provider for Subscription. Why is this happening?

Here is the code snippet I'm working on within a component: import { Component, OnDestroy, OnInit } from '@angular/core'; import { interval, Subscription } from 'rxjs'; @Component({ selector: 'app-home', templateUrl ...

Arranging an array of arrays based on the mm/dd/yyyy date field

Currently, I am facing an issue while attempting to sort data obtained from an API by date in the client-side view. Here is an example of the data being received: address: "1212 Test Ave" address2: "" checkNumber : "" city: "La Grange" country: "" email: ...

The type of props injected by WithStyles

When working on my class component, I utilize material UI withStyles to inject classes as a property. export default withStyles(styles)(myComponent) In this process, const styles = ( (theme:Theme) => createStyles({className:CSS_PROPERTIES}) I am att ...

Is it safe to use subjects in Angular like this, or are there potential security concerns?

When I find myself needing to use a variable in multiple components that can change over time, I typically store that variable in a service and encapsulate the value in a Subject. This allows every component to receive updates when next is called on the Su ...

Can TypeScript allow for type checking within type definitions?

I've developed a solution for returning reactive forms as forms with available controls listed in IntelliSense. It works well for FormControls, but I'm now looking to extend this functionality to include FormGroups that are part of the queried pa ...

Angular PrimeNG table does not successfully sort an array of class objects

The team data is initially stored in JSON format and then converted into a Team class. However, the table displaying this information is not sorted by default. Even though I have ensured that all necessary imports are included and added sortable columns, ...

WebStorm error: The type of argument 'this' cannot be assigned to the parameter type 'ObjectConstructor

After implementing the constructor below in TypeScript, I encountered an error stating that the argument type 'this' is not compatible with the parameter type 'ObjectConstructor'. Strangely enough, the CLI didn't flag any errors. U ...