【问题标题】:Type narrowing of event callback based on the event key基于事件键的事件回调类型缩小
【发布时间】:2019-12-04 11:16:27
【问题描述】:

在搜索有关如何编写类型安全事件发射器的选项时,我偶然发现了这种模式,您首先在接口中定义事件名称及其值,例如:

interface UserEvents {
    nameChanged: string;
    phoneChanged: number;
}

并且 'on' 方法被定义为接受一个 keyof UserEvents,以及一个回调,其参数是由使用称为查找类型的功能获得的键 - UserEvents[keyof T]:

function on<T extends keyof UserEvents>(e: T, cb: (val: UserEvents[T]) => void) {/**/}

提供类型安全的方法调用:

on('nameChanged', (val) => {}); // callback parameter type inferred to be a string
on('phoneChanged', (val) => {}); // ... inferred as number

我遇到的问题是在“on”方法的主体内部,我无法根据传递的键值缩小传递的回调的类型。由于事件名称及其类型是在同一个接口中定义的,因此如果我检查了键类型,我希望回调范围会缩小,但事实并非如此:

function on<T extends keyof UserEvents>(e: T, cb: (_: UserEvents[T]) => void) {
    if (e === 'nameChanged') {
        // expecting the cb type to be (string) => void
    } else {
        // and here to be (number) => void
    }

    // but regardless of the checks, the cb type
    // inside the method body is of type (string & number) => void
}

有没有什么方法可以实现基于事件键的回调的自动类型推断,使用其他 TypeScript 功能,如类型保护、区分联合等,以及这个特定的方法调用签名?

【问题讨论】:

  • 我有类似的问题......“虽然实现仍然会抱怨,因为 TypeScript 类型保护只会缩小单个值的类型。也就是说,在 if (a) { } else { } , a 的类型可能会在 then 和 else 子句中被缩小,但是当你检查 a 时 b 的类型不会被缩小,即使 a 和 b 的类型之间存在一些约束。”。检查这个question

标签: typescript


【解决方案1】:

为了实现想要的行为,我们需要将两者组合到一个数据结构中,幸运的是,当为这种结构选择 tuple 并将其与扩展函数参数相结合时,结果将完全符合需要。

考虑下面的代码:

interface UserEvents {
    nameChanged: string;
    phoneChanged: number;
}

// generic type in order to use with different objects then UserEvents only
type KeyWithCallback<A extends object> = {
  [K in keyof A]: [K, (_: A[K]) => void]
}[keyof A];

function on(...args: KeyWithCallback<UserEvents>) {
  if (args[0] === 'nameChanged') {
    const [_, clb] = args; // destructuring inside condition
    clb('') // here clb allows on string only (string) => void
  } else {
    const [_, clb] = args; // destructuring inside condition
    clb(1); // here clb allows on number only (number) => void
  }
}
// using
on('nameChanged', (a:string) => {}) // ok
on('nameChanged', (a:number) => {}) // error as expected

我使用mapped type KeyWithCallback 将所有可能的参数表示为[key, callback] 对。结果是对的并集(第 2 个元组)。

KeyWithCallback&lt;UserEvents&gt; 计算结果为:

| ["nameChanged", (_: string) => void] 
| ["phoneChanged", (_: number) => void]

第二个重要的事情是注释函数参数...args: KeyWithCallback&lt;UserEvents&gt;。从现在开始,函数体将理解函数有两个参数,一个与另一个相关,并且在联合类型的每一对中都描述了这种关系。

检查对的第一个元素会自动推断出第二个元素。

【讨论】:

  • 我们可以通过 KeysWithCallback[keyof A] 做得更好。稍后会更新我的答案
  • 第三种解决方案的大奖!我从来没有想过将事件与其回调绑定在一个元组中,并更改方法的定义以采用类型为这些元组并集的 rest 参数,并且 VsCode 在调用方法时都提供了预期的智能感知对于 if 检查
  • @Codeceps 我清理了答案,也不需要使用手动联合。所以我们有来自 KeyWithCallback 的开箱即用的联合
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-09-08
  • 2021-11-17
  • 1970-01-01
  • 2018-04-25
  • 1970-01-01
  • 1970-01-01
  • 2019-11-29
相关资源
最近更新 更多