Reputation: 8075
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
?
Upvotes: 1
Views: 410
Reputation: 328067
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)
Upvotes: 1