如何正确地对打字稿泛型进行限制

问题描述 投票:0回答:1

我正在创建这个课程(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
    时,他可以在函数的参数和返回类型中获得正确的推断。
typescript typescript-generics type-inference
1个回答
0
投票

我认为表达这一点的最直接方法是将您的 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 代码链接

© www.soinside.com 2019 - 2024. All rights reserved.