这个怎么样?描述MyClass
的期望形状和它的构造函数:
interface MyClass {
val: number;
}
interface MyClassConstructor {
new(val: number): MyClass;
(val: number): MyClass;
}
注意,MyClassConstructor
被定义为既可作为函数调用,也可作为构造函数使用。然后实现它:
const MyClass: MyClassConstructor = function(this: MyClass | void, val: number) {
if (!(this instanceof MyClass)) {
return new MyClass(val);
} else {
this!.val = val;
}
} as MyClassConstructor;
上述代码可行,但还有一些小问题。问题一:实现返回
MyClass | undefined
,编译器不知道
MyClass
返回值对应可调用函数,
undefined
值对应新构造函数,因此会报错。因此在结尾处需要加上
as MyClassConstructor
。问题二:
this
参数
目前不能通过控制流分析缩小范围,因此当设置其
val
属性时必须断言
this
不是
void
,即使此时我们知道它不可能是
void
。因此,我们必须使用
非空断言操作符!
。无论如何,您可以验证这些是否可行:
var a = new MyClass(5); // MyClass
var b = MyClass(5); // also MyClass
希望这有所帮助;祝你好运!
更新
注意:如@Paleo的answer中所提到的,如果你的目标是ES2015或更高版本,则在源代码中使用class
将在编译后的JavaScript中输出class
,并且这些需要new()
。根据规范,我曾经看到像TypeError: Class constructors cannot be invoked without 'new'
这样的错误。很可能一些JavaScript引擎忽略规范,也会接受函数式调用。如果你不关心这些注意事项(例如,你的目标明确是ES5,或者你知道你将在其中一个非规范兼容的环境中运行),那么你肯定可以强制TypeScript遵循这个规则:
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
在这种情况下,您已经将
MyClass
重命名为
_MyClass
,并定义了
MyClass
,使其既是类型(与
_MyClass
相同),又是值(与
_MyClass
构造函数相同,但其类型被断言为也可以像函数一样调用)。如上所示,这在编译时可以工作。无论您的运行时是否满意都取决于上述警告。个人而言,我会在我的原始答案中坚持函数样式,因为我知道它们在es2015及以后版本中都可调用和可newable。祝您好运!
更新 2
如果你只是想声明这个答案中的bindNew()
函数的类型,该函数接受一个符合规范的class
并生成一个既可实例化又可调用的函数,你可以像这样做:
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) {
}
这将正确地对其进行类型设置:
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
但是要注意,对于bindNew()
的重载声明并不适用于每种可能的情况。具体来说,它适用于最多有三个必需参数的构造函数。具有可选参数或多个重载签名的构造函数可能无法正确推断。因此,您可能需要根据用例调整类型。
好的,希望这有所帮助。祝你第三次好运。
2018年8月更新3
TypeScript 3.0引入了元组在剩余和展开位置,使我们能够轻松处理任意数量和类型参数的函数,而不需要以上述重载和限制。这是bindNew()
的新声明:
declare function bindNew<C extends { new(...args: A): T }, A extends any[], T>(
Class: C & { new(...args: A): T }
): C & ((...args: A) => T);
class
语法和bindNew
函数或类似我在此处回答中提到的classy-decorator
,仍然有可能调和这个问题吗?https://dev59.com/_10a5IYBdhLWcg3wHlil#48326964 - kasbahbindNew()
函数)。 - jcalzinstanceof MyClass
可能不可行吗?此外,难道没有更好的方法来输入变量数量的参数吗?总的来说,这似乎比它值得的麻烦多了 :) - kasbahinstanceof MyClass
工作,这取决于bindNew()
的实现,但我没有尝试过。 截至v2.7,TypeScript缺少一些类型运算符,需要正确地处理可变数量的参数。 无法检查函数签名或new()
签名以提取其参数类型(可能作为某种元组)和返回类型。 - jcalz