如何保证使用 Typescript 可以调用传递的对象键?

How to guarantee that passed object key is callable using Typescript?

我正在尝试为特定用例创建 React 高阶组件,问题归结为以下几点:

function sample<TObj, P extends keyof TObj, F extends keyof TObj>(
  obj: TObj,
  prop: P,
  setProp: TObj[F] extends (value: TObj[P]) => void ? F : never
) {
  obj[setProp](obj[prop]);
}

我希望能够传递一个对象,一个应该是该对象的键的字符串,以及该对象的另一个键但必须是一个函数。

这可以进一步简化为:

function sample2<TObj, F extends keyof TObj>(
  obj: TObj,
  setProp: TObj[F] extends () => void ? F : never
) {
  obj[setProp]();
}

在我看来,因为我使用了条件类型,所以可以保证 obj[setProp] 将是一个函数但是我得到错误:

This expression is not callable.
  Type 'unknown' has no call signatures.ts(2349)

如下所示,如果使用不符合要求的密钥调用该函数,该函数将出错。但同样的要求似乎并没有在函数内部应用。

我知道这可以看作是一个 XY 问题,但它让我真正感兴趣的是是否有办法使这个特定问题正常工作。

sample2() 的实现中,类型 TObj[F] extends () => void ? F : never 是一个未解析的条件类型。也就是说,它是一种条件类型,取决于要解析的当前未指定的泛型类型参数。在这种情况下,编译器通常不知道如何处理它并将其视为本质上不透明。 (有关此的一些讨论,请参阅 microsoft/TypeScript#23132。)特别是它没有意识到 TObj[Tobj[F] extends ()=>void ? F : never] 最终必须解析为 ()=>void.

的某些子类型

通常我会完全避免条件类型,除非它们是必要的。编译器可以更容易地从 mapped types 中理解和推断像 Record<K, V>:

function sample2<K extends PropertyKey, T extends Record<K, () => void>>(
  obj: T,
  prop: K
) {
  obj[prop]();
}

当你调用它时,它的行为类似:

const obj2 = {
  func() { console.log("func") },
  prop: 42
};
sample2(obj2, "func"); // okay, 
//sample2(obj, "prop"); // error
//      ~~~ <-- number is not assignable to ()=>void

编辑:为了解决原来的 sample(),我会使用这个定义:

function sample<
  PK extends PropertyKey,
  FK extends PropertyKey,
  T extends Record<PK, any> & Record<FK, (v: T[PK]) => void>
>(
  comp: T,
  prop: PK,
  setProp: FK
) {
  comp[setProp](comp[prop]);
}

const obj = {
  func(z: number) { console.log("called with " + z) },
  prop: 42
}

我认为这也符合您的要求:

sample(obj, "prop", "func"); // called with 42
sample(obj, "prop", "prop"); // error!
//     ~~~ <-- number not assignable to (v: number)=>void
sample(obj, "func", "func"); // error!
//     ~~~ <-- (v: number)=>void not assignable to number

好的,希望对你有帮助;祝你好运!

Link to code