Tricky decorator class

296 Views Asked by At

I have the following code snippet which i find it quite hard to understand:

  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();
            }
        });
    };
}

I am having difficult time making sense of above code and understood as much as the following:

Model returns type T (I know what generics are so don't worry about explaining generics) in which

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

What does above mean? T going to keep existing properties plus extra added features?

In addition can you explain the function return type? Are we adding 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)._completeInitialization();
            }
        });
2

There are 2 best solutions below

0
On BEST ANSWER

T extends { new(...args: any[]): {} } means that T must be a constructor function (ie a class). The constructor arguments as well as return type don't matter (T can have any number of arguments and may return any type that extends {}, in effect any object type).

If invoked directly the T will be the class. The approach being used to type this decorator is basically that of mixins (described for typescript here).

The return value of the function will be a new class that inherits the decorated class. As such it's not adding a constructor but rather replacing the original constructor with a new one, and calling the original constructor through the super call.

The generics are a bit of overkill in this situation in my opinion. They are useful for mixins, because they forward the original class from input parameter to output parameter (and the mixin adds members to the type). But since decorators can't change the structure of the type, there is nothing to forward. Also instead of the constructor returning {} I would type it to return Record since you check that at runtime time, might as well check it at compile time as well:

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  { }
0
On

Type Constraint

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

Here, the type T is constrained to any type that extends { new(...args: any[]): {} }. The formatting here can be a bit confusing – properly formatted, the type looks like this:

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

This describes a so called newable, which is some kind of function object that needs to be invoked using new. For example:

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

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

The ...args: any[] is simply a rest parameter declaration, the return type declaration, {} means that an object needs to be returned. TypeScript will assume that the returned object has no properties whatsoever.

Anonymous Class in Return

As for the return type: Since the Model decorator function is class decorator, it can return a class itself. If it does return a class, that class will be used instead of the decorated class.

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

from the TS handbook

For example:

// `ctr` is a common abbreviation for "constructor"
function Decorate(ctr: Function) {
    return class {
        constructor() {
            super();
            console.log('decorated');
        }
    };
}

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

new A(); // this will log: "A" first, then "decorated"