Afaik,没有安全方法可以在不改变函数体或使用类型断言的情况下做你想做的事。
为了验证函数参数,首先我们需要从Foo获取所有方法键:
class Foo {
var1: string = 'var1';
var2: string = 'var2';
hello(request: string) { }
world(request: number) { }
}
// This type reflects any function/method
type Fn = (...args: any[]) => any
type ObtainMethods<T> = {
[Prop in keyof T]: T[Prop] extends Fn ? Prop : never
}[keyof T]
// "hello" | "world"
type AllowedMethods = ObtainMethods<Foo>
让我们测试一下:
const executeFoo = <Method extends ObtainMethods<Foo>>(
methodName: Method
) => { }
executeFoo('hello') // ok
executeFoo('world') // ok
executeFoo('var1') // expected error
但是,第二个参数有问题:
const executeFoo = <Method extends ObtainMethods<Foo>>(
methodName: Method, parameter: Parameters<Foo[Method]>[0]
) => {
// Argument of type 'string | number' is not assignable to parameter of type 'never'. Type 'string' is not assignable to type 'never'.
foo[methodName](parameter)
}
您可能已经注意到,有一个错误。
Argument of type 'string | number' is not assignable to parameter of type 'never'.
Type 'string' is not assignable to type 'never'.
这很重要。如果您尝试调用foo[methodName](),您将看到此函数需要never 作为第一个参数的类型。这是因为
同样,同一类型变量在逆变位置的多个候选会导致推断出交集类型。
您可以在我的第一部分article 中找到更多信息。这是因为 TS 不知道您使用的是哪个 methodName。因此,TS 编译器与方法中的所有参数相交:string & number,因为这是使函数签名安全的唯一安全方法。
所以,您希望在方法中使用什么类型的参数非常重要。
如何解决?
在这个特定的例子中,我相信使用type assertion 是合理的:
const executeFoo = <Method extends ObtainMethods<Foo>>(
methodName: Method, parameter: Parameters<Foo[Method]>[0]
) => {
(foo[methodName] as (arg: Parameters<Foo[Method]>[0]) => void)(parameter)
}
executeFoo('hello', 'str') // ok
executeFoo('world', 42) // ok
executeFoo('world', "42") // expected error
executeFoo('var1') // expected error
Playground
如果你对函数参数推断感兴趣,可以查看我的blog
也可以使用条件语句进行类型缩小(适用于 TS >= 4.6)
type Fn = (...args: any[]) => any
type ObtainMethods<T> = {
[Prop in keyof T]: T[Prop] extends Fn ? Prop : never
}[keyof T]
// "hello" | "world"
type AllowedMethods = ObtainMethods<Foo>
type Values<T> = T[keyof T]
type AllowedArguments = {
[Method in AllowedMethods]: [Method, Parameters<Foo[Method]>[0]]
}
const foo = new Foo();
const executeFoo = (
...[name, arg]: Values<AllowedArguments>
) => {
if (name === 'hello') {
foo[name](arg)
} else {
foo[name](arg)
}
}
executeFoo('hello', 'str') // ok
executeFoo('world', 42) // ok
executeFoo('world', "42") // expected error
executeFoo('var1') // expected error
但这并没有多大意义。