Search code examples
typescriptmixins

Implement mixins in TypeScript without using any type


I am wrangling with this mixin-creating TypeScript code:

function applyMixins(derivedCtor: Function, constructors: Function[]) {
    //Copies methods
    constructors.forEach((baseCtor) => {
      Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
        Object.defineProperty(
          derivedCtor.prototype,
          name,
          Object.getOwnPropertyDescriptor(baseCtor.prototype, name) ||
            Object.create(null)
        );
      });
    });
    //Copies properties
    constructors.forEach((baseCtor) => {
      let empty = new baseCtor();
      Object.keys(empty).forEach((name) => {
        Object.defineProperty(
          derivedCtor.prototype,
          name,
          Object.getOwnPropertyDescriptor(empty, name) ||
            Object.create(null)
        );
      });
    });
  }

It does not compile, complaining about this row: let empty = new baseCtor();: TS2351: This expression is not constructable. Type 'Function' has no construct signatures.. I know that this can be fixed by swapping both Function type references in the 1st row with any, but I'm trying to live my life without any, as it often is a sloppy way to tell TypeScript to shut up.

Is there a way to implement this code without using any?


Solution

  • The problem is that Function is a very wide type and includes any function, including ones which cannot be called via new. Therefore the compiler is complaining that Function is not constuctible. The solution is therefore to use a type which is known to have a construct signature. In TypeScript, such a signature is represented by prepending the keyword new to a function signature:

    type NewableFunctionSyntax = new () => object;
    type NewableMethodSyntax = { new(): object };
    

    Those types both represent a constructor that accepts no arguments and produces an instance of type assignable object. Note that while those syntaxes are different, they are essentially the same. (To see this, note that the compiler allows you to declare a var multiple times but will complain if you annotate it with different types. The fact that the following compiles with no error,

    var someCtor: NewableFunctionSyntax;
    var someCtor: NewableMethodSyntax; // no error
    

    is an indication that the compiler treats NewableFunctionSyntax and NewableMethodSyntax as essentially interchangeable.)


    By changing Function to one of these, your code now compiles with no errors:

    function applyMixins(derivedCtor: { new(): object }, constructors: { new(): object }[]) {
        //Copies methods
        constructors.forEach((baseCtor) => {
            Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
                Object.defineProperty(
                    derivedCtor.prototype,
                    name,
                    Object.getOwnPropertyDescriptor(baseCtor.prototype, name) ||
                    Object.create(null)
                );
            });
        });
        //Copies properties
        constructors.forEach((baseCtor) => {
            let empty = new baseCtor();
            Object.keys(empty).forEach((name) => {
                Object.defineProperty(
                    derivedCtor.prototype,
                    name,
                    Object.getOwnPropertyDescriptor(empty, name) ||
                    Object.create(null)
                );
            });
        });
    }
    

    Let's test out calling applyMixins() to make sure we understand what {new(): object} does and does not match:

    class Works {
        x = 1;
        constructor() { }
    }
    applyMixins(Works, []); // okay
    

    Works is fine because it is a class constructor which takes no parameters.

    class CtorRequiresArg {
        y: string;
        constructor(y: string) { this.y = y; }
    }
    
    applyMixins(CtorRequiresArg, []); // error!
    // -------> ~~~~~~~~~~~~~~~
    // Type 'new (y: string) => CtorRequiresArg' 
    // is not assignable to type 'new () => object'
    

    CtorRequiresArg fails because you have to pass a string argument when you construct it, like new CtorRequiresArg("hello")... but applyMixins() only accepts constructors that can be called without any arguments.

    And finally:

    function NotACtor() { }
    
    applyMixins(NotACtor, []); // error!
    // -------> ~~~~~~~~
    // Type '() => void' provides no match 
    // for the signature 'new (): object'
    

    NotACtor fails because it is not considered constructible. This may be surprising because at runtime nothing will stop you from calling new NotACtor(), but it is the compiler's opinion that if you wanted a class constructor you would be using class notation in .ts files... even when targeting an ES5 runtime, since TypeScript will down-level it for you automatically. (See microsoft/TypeScript#2310 for more information)


    Playground link to code