作为一个用例,我希望允许人们向对象添加任意函数,并使用其名称和参数通过另一个函数调用该函数。
// Example arbitrary function
const sum = (a: number, b: number): number => a + b
// Another such function
const saySomething = (): string => Math.random() < 0.5 ? 'morning' : 'evening'
// This object holds the functions declared above
const execFn: Record<string, (...args: any[]) => any> = { sum, saySomething }
// These functions can be called through a generic function
export const exec = (name: string, ...args: any[]): any => execFn[name](...args)
console.log(exec('sum', 1, 2))
这可行,但它不是类型安全的。例如,有人可能输入了错误的参数。我可以使用映射类型来改进这一点,如下所示:
type GenericFn = keyof typeof execFn
type GenericParameters = {
[K in GenericFn]: Parameters<(typeof execFn)[K]>
}
type GenericReturn = {
[K in GenericFn]: Parameters<(typeof execFn)[K]>
}
然后修改
exec
函数签名:
// These functions can be called through a generic function
export const exec = <T extends GenericFn>(
name: T,
...args: GenericParameters[T]
): GenericReturn[T] => (execFn[name] as (...args: any[]) => any)(...args)
现在,TS 将正确判断是否有人在没有正确参数的情况下调用函数,并且返回类型已正确键入。
console.log(exec('sum', 1)) // Expected 3 arguments, but got 2. ts(2554)
如何删除
exec
函数中的类型断言?
// These functions can be called through a generic function
export const exec = <T extends GenericFn>(
name: T,
...args: GenericParameters[T]
): GenericReturn[T] => execFn[name](...args)
删除类型断言会引发以下 TS 错误。
Type 'string | number' is not assignable to type 'GenericReturn[T]'.
Type 'string' is not assignable to type 'GenericReturn[T]'.
Type 'string' is not assignable to type 'never'.
The intersection '[a: number, b: number] & []' was reduced to 'never'
because property 'length' has conflicting types in some constituents.ts(2322)
尝试隔离函数给我的印象是通用函数没有从对象中获取精确的函数。
const fn = execFn[name]
// const fn: {
// sum: (a: number, b: number) => number;
// saySomething: () => string;
// }[T]
这实际上非常接近某些处理输入输出类型依赖性的推荐方法,如 microsoft/TypeScript#47109 中所述。为了使以下代码正常工作:
export const exec = <K extends GenericFn>(
name: K,
...args: GenericParameters[K]
): GenericReturn[K] => execFn[name](...args)
编译器需要将
execFn[name]
视为泛型类型 (...args: GenericParameters[K]) => GenericReturn[K]
的单个函数。这意味着它需要看到 execFn
是由 K
类型的键索引时的一般行为。
根据 microsoft/TypeScript#47109,如果
execFn
表示为 K
中的 GenericFn
键上的 映射类型,则会发生这种情况。具体来说:
const execFn: {
[K in GenericFn]: (...args: GenericParameters[K]) => GenericReturn[K]
} = ⋯;
由于您的
GenericParameters
和 GenericReturn
类型本身就是用 typeof execFn
编写的,因此我们无法在没有循环的情况下直接这样做。让我们将您的 execFn
重命名为 _execFn
:
const _execFn = {
isEven, randomInt, repeat, saySomething, sum,
} as const;
type GenericFn = keyof typeof _execFn
type GenericParameters = {
[K in GenericFn]: Parameters<(typeof _execFn)[K]>
}
type GenericReturn = {
[K in GenericFn]: ReturnType<(typeof _execFn)[K]>
}
现在我们可以将
_execFn
分配给适当类型的 execFn
:
const execFn: {
[K in GenericFn]: (...args: GenericParameters[K]) => GenericReturn[K]
} = _execFn;
这有效。
请注意,这取决于 execFn
类型的
form。如果您通过 IntelliSense 查看
execFn
的类型:
/* const execFn: {
isEven: (n: number) => boolean;
randomInt: (upTo: number) => number;
repeat: (word: string, times: number) => string[];
saySomething: () => string;
sum: (a: number, b: number) => number;
} */
并将其与显示的类型进行比较
_execFn
:
/* const _execFn: {
readonly isEven: (n: number) => boolean;
readonly randomInt: (upTo: number) => number;
readonly repeat: (word: string, times: number) => string[];
readonly saySomething: () => string;
readonly sum: (a: number, b: number) => number;
} */
这些基本上是相同的(
readonly
在这里并不重要)。当然,它们必须如此,否则 const execFn: ⋯ = _execFn
作业将会失败。
但是类型的内部表示是不同的。
execFn
的类型明确是映射类型,其中每个属性都具有通用关系,而 _execFn
的类型只是可能不相关的属性的列表。编译器缺乏查看 _execFn
并自行得出 execFn
类型的能力;需要明确告知。
所以
execFn[name]
的类型是一个接受GenericParameters[K]
类型的参数列表的单一函数,这很好。但是 _execFn[name]
的类型最终会扩展为函数的 union ,这是非常无用的,因为函数的并集只能通过参数的 intersection 来安全地调用(参见 TS3.3 发行说明) 。因此 _execFn[name]
想要一个 never
类型的参数列表(即,没有安全的方法来调用这个函数联合,因为没有参数适用于每个调用签名)。
事实上,最终导致 microsoft/TypeScript#47109 方法的问题是关于这些无用的函数联合,如 microsoft/TypeScript#30581 中所述。这个问题用“相关联合”来表述,其中您有一个像
v
这样的联合类型的值 {k: "sum", fn: (a: number, b: number) => number, args: [a: number, b: number]} | {k: "saySomething", fn: () => string, args: []} | ⋯}
并且您想调用 v.fn(...v.args)
,但编译器不允许您这样做。通用索引是解决方案的一部分,但您还需要映射类型。