我正在创建这个课程(playground链接):
export class CascadeStrategies<
T extends Record<any, (...args: any[]) => unknown>
> {
private strategies: T = {} as T;
constructor(strategyMap: T) {
this.registerStrategies(strategyMap);
}
private registerStrategies(strategyMap: T) {
this.strategies = strategyMap;
}
use(
strategies: (keyof T)[],
...args: Parameters<T[keyof T]>
): ReturnType<T[keyof T]> {
return this.strategies[strategies[0]](...args);
}
}
这个类的预期用途应该是
const myMap = {
test: (arg1: number, arg2: string) => arg1,
otherTest: (arg1: number, arg2: string) => arg2,
thirdTest: (arg1: number, arg2: string) => null
}
const cascadeStrats = new CascadeStrategies(myMap);
const shouldBeNumber = cascadeStrats.use(["test"], 0, "");
const shouldBeString = cascadeStrats.use(["otherTest"], 0, "");
const shouldBeNull = cascadeStrats.use(["thirdTest"], 0, "");
我希望
T
成为一个对象,其条目是可以接受同一组参数并返回 string
的函数,因此我使用 T extends Record<any, (...args: unknown[]) => string
。this.strategies[strategies[0]](...args)
的类型为 unknown
,与预期的 ReturnType<T[keyof T]>
不兼容。
如果我将
strategies
的类型从 T
更改为 Record<any, any>
,则 this.strategies[strategies[0]](...args)
将具有正确的类型,并在使用时正确推断。尽管 strategies
只是一个内部变量,在使用该类时不会影响 DX,但我想知道我在这里缺少什么来实现所需的结果:
strategyMap
(即,其条目是接受相同参数集并返回 string
的函数的对象)。strategies
没有 Record<any, any>
类型。cascadeStrats.use
时,他可以在函数的参数和返回类型中获得正确的推断。我认为表达这一点的最直接方法是将您的 generic 类型参数分成两部分。您可以拥有
A
(所有策略通用的参数列表类型)和 T
(从策略键到相应策略的返回类型的映射)。给定这些类型,那么 strategies
将是类型
type Strategies<A extends any[], T> =
{ [K in keyof T]: (...args: A) => T[K] }
这是一个映射类型,将
T
的每个成员转换为返回该成员的函数。
这就是我的机会
CascadeStrategies
:
class CascadeStrategies<A extends any[], T extends object> {
private strategies!: Strategies<A, T>
constructor(strategyMap:
Record<string, (...args: A) => any> &
Strategies<A, T>
) {
this.registerStrategies(strategyMap);
}
private registerStrategies(strategyMap: Strategies<A, T>) {
this.strategies = strategyMap;
}
use<K extends keyof T>(
strategies: K[],
...args: A
) {
return this.strategies[strategies[0]](...args); // okay
}
}
编译没有错误。这里重要的部分是内部发生的事情
use()
。现在该函数在 K
中是通用的,是 T
的一个键。根据需要推断 use()
的返回类型 T[K]
。
请注意,为了使其正常工作,当您编写
A
时,我们需要 TypeScript 来推断 T
和 new CascadeStrategies(myMap)
。推理可能很棘手。我的方法是使构造函数参数的类型为Record<string, (...args: A) => any> & Strategies<A, T>
。这是一个“交集”,其中每个部分都有助于推断不同的类型参数。 Record<string, (...args: A) => any>
类型允许推断 A
,因为它可以在 TypeScript 了解有关 T
之前发生。然后 Strategies<A, T>
允许从方法的返回类型推断 T
。将交集X & Y
扩大到其成员之一Y
总是安全的,因此我们可以处理复杂的交集并将strategyMap
视为类型Strategies<A, T>
。
让我们测试一下:const myMap = {
test: (arg1: number, arg2: string) => arg1,
otherTest: (arg1: number, arg2: string) => arg2,
thirdTest: (arg1: number, arg2: string) => null
}
const cascadeStrats = new CascadeStrategies(myMap);
/* ^? const cascadeStrats: CascadeStrategies<
[arg1: number, arg2: string],
{ test: number; otherTest: string; thirdTest: null; }
>
*/
const shouldBeNumber = cascadeStrats.use(["test"], 0, "");
// ^? const shouldBeNumber: number
const shouldBeString = cascadeStrats.use(["otherTest"], 0, "");
// ^? const shouldBeString: string
const shouldBeNull = cascadeStrats.use(["thirdTest"], 0, "");
// ^? const shouldBeNull: null
const shouldBeStringOrNumber = cascadeStrats.use(["test", "otherTest"], 0, "")
// ^? const shouldBeStringOrNumber: string | number
cascadeStrats.use(["test"], "oops", "abc"); // error!
// -----------------------> ~~~~~~
// Argument of type 'string' is not assignable to parameter of type 'number'.
看起来不错。
T
被正确推断,因此
shouldBeXXX
都是预期的类型,并且 A
也被正确推断,以便编译器注意到您是否传入了错误的参数类型(如上面最后一行所示) .Playground 代码链接