我到处找过,但没有找到如何实现这种行为的线索。我是打字新手,如果这是个愚蠢的问题,请原谅我。
下面的代码是我想要的一个非常简化的版本:
const library = { // could also be a class if necessary
registerMethod(name: string, method: () => any): void {
this[name] = method;
}
}
library.registerMethod('couldBeWhatever', () => { }); // could be called any number of times with custom functions and names
我正在寻找一种方法(不一定是上面的方法),在启动时注册类或对象的方法,并允许完成这些动态函数。这在打字中可能吗?
我猜通过"在运行时";您的意思是希望编译器执行控制流分析,试图通过检查调用libray.registerMethod()
的TypeScript代码来预测哪些方法可用。显然,对于那些只有在运行时才知道的东西,没有办法在IDE中完成,例如,如果您的代码生成了一个带有Math.random()
的随机方法名称字符串,或者从用户输入或API响应或其他内容中提取了方法名称。
如果是这样,那么您可能希望使用断言函数。断言函数会缩小其中一个参数的类型(您也可以使用断言方法来缩小调用它的对象的类型)。
通过缩小范围,这意味着断言函数之后的表观类型必须是断言函数之前表观类型的子类型。例如,您不能使断言函数将string
变成number
,但可以使其将string | number
变成number
。因此,使用控制流分析对library
突变进行建模的一个注意事项是,它不能用来使其与原始类型不兼容。registerMethod()
方法看起来会向library
添加新成员,而向对象类型添加成员是一种缩小范围的形式。但是,如果您需要一个alterMethod()
,也就是说,将注册方法的类型从() => string
更改为() => number
,您将无法做到这一点。
以下是library
的示例实现,registerMethod()
作为断言方法:
interface _Lib<T extends Record<keyof T, () => any>> {
registerMethod<K extends string, M extends () => any>(
name: K,
method: K extends keyof T ? T[K] : M
): asserts this is Library<{
[P in K | keyof T]: P extends K ? M : P extends keyof T ? T[P] : never
}>
}
type Library<T extends Record<keyof T, () => any>> = T & _Lib<T>;
const library: Library<{}> = {
registerMethod(this: Library<any>, name: string, method: () => any): void {
this[name] = method;
}
}
其中有很多内容,但基本思想是library
从Library<{}>
开始,只有一个registerMethod()
方法。当您在名称为K
类型、方法为M
类型的Library<T>
上调用registerMethod()
时,编译器将将该Library<T>
缩小到类似Library<T & Record<K, M>>
的范围。这意味着,除了具有registerMethod()
和T
中的任何内容外,它现在还在密钥K
处具有一个成员,其类型为M
。
您可以测试它是否工作:
library.registerMethod('couldBeWhatever', () => "hello");
console.log(library.couldBeWhatever().toUpperCase()); // HELLO
library.somethingElse; // error, somethingElse does not exist
library.registerMethod('somethingElse', () => 123);
console.log(library.couldBeWhatever().toUpperCase()); // still HELLO
console.log(library.somethingElse().toFixed(2)); // "123.00"
万岁,它起作用了!不过,断言函数也有一些注意事项,它们对您的用例可能很重要,也可能无关紧要。
首先,为了使用断言函数,您需要对它或调用它的对象进行手动注释,而不是推断。这就是为什么我必须把上面的library
注释为Library<{}>
。这是TypeScript目前的设计限制。有关详细信息,请参阅microsoft/TypeScript#36931等。
接下来,与TypeScript中的所有控制流分析一样,编译器并不完全知道。它不会通过模拟运行程序的所有可能方式来执行控制流分析,并查看哪些可能的类型窄化在所有范围内都保持正确。这样做的代价太高了。现在,编译器所做的是:当你越过函数边界时,编译器会重置任何控制流变窄。这是一个合理的权衡,因为编译器通常无法确定函数体何时会相对于函数体之外的代码进行调用,反之亦然。有关此问题的讨论,请参阅microsoft/TypeScript#9998。
这对上面的代码意味着什么:当你用library
注册方法时,你将能够在同一个函数范围内使用它们。但你不能使用它们"之后";在一些任意的其他范围内,如功能体或其他模块:
library.registerMethod('somethingElse', () => 123);
function oops() {
library.somethingElse() // the compiler doesn't know about this
}
编译器真的不知道,当调用oops()
时,somethingElse
已经在library
上注册。事实上,如果不检查程序中所有的代码,我也不知道。
解决方法是在某个地方进行所有注册,然后";冻结";结果库的类型由";节省";将其转换为新的CCD_ 34变量。
const registeredLibrary = library;
function okay() {
registeredLibrary.somethingElse(); // the compiler does know about this
}
这是因为registeredLibrary
是作为library
的副本创建的,其作用域中已经注册了方法。代码中没有registeredLibrary
不能拥有这两个额外方法的地方,所以编译器很乐意在okay()
函数体中使用它们。
上述注意事项很可能使其不适用于您。编译器非常强大,但无法处理需要在任何地方同时进行的分析,因为代码可以以各种可能的方式运行。但是断言方法至少可以在一定程度上为这种"断言"建模;动态的";行为
游乐场链接到代码