What about this? Describe the desired shape of MyClass
and its constructor:
interface MyClass {
val: number;
}
interface MyClassConstructor {
new(val: number): MyClass; // newable
(val: number): MyClass; // callable
}
Notice that MyClassConstructor
is defined as both callable as a function and newable as a constructor. Then implement it:
const MyClass: MyClassConstructor = function(this: MyClass | void, val: number) {
if (!(this instanceof MyClass)) {
return new MyClass(val);
} else {
this!.val = val;
}
} as MyClassConstructor;
The above works, although there are a few small wrinkles. Wrinkle one: the implementation returns MyClass | undefined
, and the compiler doesn't realize that the MyClass
return value corresponds to the callable function and the undefined
value corresponds to the newable constructor... so it complains. Hence the as MyClassConstructor
at the end. Wrinkle two: the this
parameter does not currently narrow via control flow analysis, so we have to assert that this
is not void
when setting its val
property, even though at that point we know it can't be void
. So we have to use the non-null assertion operator !
.
Anyway, you can verify that these work:
var a = new MyClass(5); // MyClass
var b = MyClass(5); // also MyClass
Hope that helps; good luck!
UPDATE
Caveat: as mentioned in @Paleo's answer, if your target is ES2015 or later, using class
in your source will output class
in your compiled JavaScript, and those require new()
according to the spec. I've seen errors like TypeError: Class constructors cannot be invoked without 'new'
. It is quite possible that some JavaScript engines ignore the spec and will happily accept function-style calls also. If you don't care about these caveats (e.g., your target is explicitly ES5 or you know you're going to run in one of those non-spec-compliant environments), then you definitely can force TypeScript to go along with that:
class _MyClass {
val: number;
constructor(val: number) {
if (!(this instanceof MyClass)) {
return new MyClass(val);
}
this.val = val;
}
}
type MyClass = _MyClass;
const MyClass = _MyClass as typeof _MyClass & ((val: number) => MyClass)
var a = new MyClass(5); // MyClass
var b = MyClass(5); // also MyClass
In this case you've renamed MyClass
out of the way to _MyClass
, and defined MyClass
to be both a type (the same as _MyClass
) and a value (the same as the _MyClass
constructor, but whose type is asserted to also be callable like a function.) This works at compile-time, as seen above. Whether your runtime is happy with it is subject to the caveats above. Personally I'd stick to the function style in my original answer since I know those are both callable and newable in es2015 and later.
Good luck again!
UPDATE 2
If you're just looking for a way of declaring the type of your bindNew()
function from this answer, which takes a spec-conforming class
and produces something which is both newable and callable like a function, you can do something like this:
function bindNew<C extends { new(): T }, T>(Class: C & {new (): T}): C & (() => T);
function bindNew<C extends { new(a: A): T }, A, T>(Class: C & { new(a: A): T }): C & ((a: A) => T);
function bindNew<C extends { new(a: A, b: B): T }, A, B, T>(Class: C & { new(a: A, b: B): T }): C & ((a: A, b: B) => T);
function bindNew<C extends { new(a: A, b: B, d: D): T }, A, B, D, T>(Class: C & {new (a: A, b: B, d: D): T}): C & ((a: A, b: B, d: D) => T);
function bindNew(Class: any) {
// your implementation goes here
}
This has the effect of correctly typing this:
class _MyClass {
val: number;
constructor(val: number) {
this.val = val;
}
}
type MyClass = _MyClass;
const MyClass = bindNew(_MyClass);
// MyClass's type is inferred as typeof _MyClass & ((a: number)=> _MyClass)
var a = new MyClass(5); // MyClass
var b = MyClass(5); // also MyClass
But beware the the overloaded declarations for bindNew()
don't work for every possible case. Specifically it works for constructors which take up to three required parameters. Constructors with optional paramaters or multiple overload signatures will probably not be properly inferred. So you might have to tweak the typings depending on use case.
Okay, hope that helps. Good luck a third time.
UPDATE 3, AUG 2018
TypeScript 3.0 introduced tuples in rest and spread positions, allowing us to easily deal with functions of an arbitrary number and type of arguments, without the above overloads and restrictions. Here's the new declaration of bindNew()
:
declare function bindNew<C extends { new(...args: A): T }, A extends any[], T>(
Class: C & { new(...args: A): T }
): C & ((...args: A) => T);