从 typescript type/interface 中提取键,其中值的类型为 T

Pulling keys out of typescript type/interface where the values are of type T

我正在开发一个包含一组回调函数的新库。我想实现一种“命名空间”功能,这样接口和回调链就不会变得如此庞大。

我的回调类型基本回调/命名空间如下所示:

type AnyFunction = (...args: any) => void;

type DefinedTypeMap = {
  [key: string]: AnyFunction | DefinedTypeMap;
};

当时的扩展接口可能如下所示:

interface IMyCallbackFunctions extends DefinedTypeMap {
  namespace: {
    func: (msg: string) => void;
  };
  root: (msg: string) => void;
  otherRoot: (msg: string) => void;
}

我正在开发一个 class 函数,如果该值是 AnyFunction 类型(不是嵌套的命名空间),该函数将只接受对象上的键。

由于接口是由开发人员使用库定义的(只要它们扩展了所需的接口),在这种情况下接受的函数参数将仅为 "root" | "otherRoot" 我已尝试 Omit<T, K> 作为以及寻找解决方案的堆栈溢出(无济于事)。

这个库使用 Typescript@4.5.5

当前函数(不支持正在进行的命名空间功能)如下所示:

on<E extends keyof P2PConnectionEventMap<T>, F extends P2PConnectionEventMap<T>[E]>(event: E, callback: F) => void

并且应修改为仅允许 event 参数接受接口上的值为回调类型而非对象类型的键。

在此先感谢您的帮助,这让我想拔头发。

让我们定义一个名为 FuncKeys<T> 的类型实用程序,它采用对象类型 T 和 returns 扩展 DefinedTypeMap 接口上的 union of keys where the type of the property at each such key is assignable to AnyFunction. The goal is that FuncKeys<IMyCallbackFunctions> should evaluate to "root" | "otherRoot" and that it should not include "namespace" (which is a non-function object) or string (from the index signature)。 =60=]

明确地说,您的 IMyCallbackFunctions 接口等同于:

interface IMyCallbackFunctions {
  [key: string]: DefinedTypeMap | AnyFunction;
  namespace: {
    func: (msg: string) => void;
  };
  root: (msg: string) => void;
  otherRoot: (msg: string) => void;
}

我刚刚明确添加了索引签名。


通常当遇到这样的问题时,我会指向 KeysMatching<T, V> 实用程序,如 . But that utility doesn't work on types with string index signatures; the fact that every possible string is a valid key for the object type tends to "wash away" any results from the specific literal 键的答案中给出的那样。所以我们必须采取不同的方法。

这是我为您的用例使用的实现:

type FuncKeys<T extends object> = 
  keyof { [K in keyof T as T[K] extends AnyFunction ? K : never]: any };

这个uses key remapping to filter out keys where the property is not assignable to AnyFunction. Each key K in the keys of the object type T is "remapped" to either itself (if the property type you get when [indexing into]https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html) T with a key of type K, a.k.a. T[K] is assignable to AnyFunction), or to the never type (otherwise). This effectively keeps only the keys you want and removes the ones you don't. Note that the mapped type we come up with has just any as its property types, but that doesn't matter because we are just grabbing the keys with the keyof type operator.

让我们来看看它是如何与 IMyCallbackFunctions 一起工作的。看起来像

  keyof { [K in keyof IMyCallbackFunctions as IMyCallbackFunctions[K] extends AnyFunction ? K : never]: any };

所以 K 将遍历 string"namespace""root""otherRoot"。对于 string"namespace",测试 IMyCallbackFunctions[K] extends AnyFunction ? K : never 将评估为 never,因为 (DefinedTypeMap | AnyFunction) extends AnyFunction 不正确,{func: (msg: string) => void;} extends AnyFunction 也不正确。所以那些钥匙被丢弃了。对于 "root""otherRoot",测试将分别评估为 "root""otherRoot",因为 ((msg: string) => void) extends AnyFunction 为真。

这意味着我们现在有

  keyof { root: any, otherRoot: any }

最终根据需要评估为 "root" | "otherRoot"


然后您可以使用 FuncKeys<T> 使 on() 方法的调用签名仅接受值为 function-typed.

的键

Playground link to code