对不同事件类型使用通用回调时TypeScript类型错误

cuxqih21  于 2023-06-07  发布在  TypeScript
关注(0)|答案(1)|浏览(125)

我想做一个通用的事件处理程序,我希望能够指定像“pointermove”这样的事件键,并通过类型脚本来推断事件类型,在本例中是PointerEvent,但是当使用多个事件时,我会得到一个错误。
这里有一个最小可重复的例子

export type ContainedEvent< K extends keyof HTMLElementEventMap> = {
    eventName: K;
    callback: ContainedEventCallback< K>;
  
};
export type ContainedEventCallback< K extends keyof HTMLElementEventMap> = (
    event: HTMLElementEventMap[K],

) => void;
export default function useContainedMultiplePhaseEvent<
    K extends keyof HTMLElementEventMap = keyof HTMLElementEventMap
>(
    el: HTMLElement ,
    events: ContainedEvent<K>[],
) {
    
  for (const e of events) {
      el.addEventListener(e.eventName, (ev) => e.callback(ev));
  }     
}
const div = document.createElement("div");
 const doA: ContainedEventCallback<"pointerdown"> = (
        e,
    ) => {
      console.log("A")
    };
 const doB: ContainedEventCallback<"pointermove"> = (
        e,
    ) => {
      console.log("B")
    };

useContainedMultiplePhaseEvent(div,
        [
            {
                eventName: "pointerdown",
                callback: doA,
            },
            {
                eventName: "pointermove",
                callback: doB,
            }
        ]
    );
zujrkrfu

zujrkrfu1#

我认为这里的主要问题是,当TypeScript从数组字面量推断genericelement 类型时,它只查询数组的第一个元素。这通常是人们想要的,因为它允许像function foo<T>(...args: T[]) {}这样的东西接受foo("a", "b", "c")foo(1, 2, 3),但拒绝foo("a", 2, "c")。也就是说,当从T[]类型的值推断T时,编译器将仅推断 homogeneous 数组。但是在你的例子中,你想允许一个 hetereogenous 数组。
在这种情况下,通常的方法是更改泛型类型参数,使其引用整个数组,而不仅仅是数组的元素类型。在您的例子中,这意味着不是让K引用ContainedEvent的类型参数,而是让它引用ContainedEvent s元组的类型参数的元组。这意味着K将类似于["pointerdown", "pointermove"],而events将是[ContainedEvent<"pointerdown">, ContainedEvent<"pointermove">]类型。
像这样:

function useContainedMultiplePhaseEvent<K extends readonly (keyof HTMLElementEventMap)[]>(
    el: HTMLElement, events: [...{ [I in keyof K]: ContainedEvent<K[I]> }],
) {
    for (const e of events) {
        el.addEventListener(e.eventName, (ev) => e.callback(ev));
    }
}

因此events的类型是Map的元组类型,其中K[I]的类数字索引I处的K的每个元素用ContainedEvent Package 以得到ContainedEvent<K[I]>。哦,events的类型被 Package 在可变元组类型[...⋯]中,以给予编译器我们希望events的类型被推断为元组而不是无序数组(此行为在ms/TS#39094中描述)。
让我们试试看:

useContainedMultiplePhaseEvent(div, [
    { eventName: "pointerdown", callback: doA, },
    { eventName: "pointermove", callback: doB, }
]); // okay
// useContainedMultiplePhaseEvent<["pointerdown", "pointermove"]>

看起来不错!
这就回答了我的问题。
还有其他可能的方法,但我不想偏离您的原始代码太远。由于ContainedEvent<K>中的K实际上只能是固定联合keyof HTMLElementEventMap中的一个,因此您实际上可以将ContainedEvent本身变成一个联合,可能是通过将定义更改为ms/TS#47109中描述的 * 分布式对象类型 *。那么你的useContainedMultiplePhaseEvent函数就不需要是泛型的,因为events的每个元素都是union类型ContainedEvent。它看起来像这样:

type ContainedEvent<K extends keyof HTMLElementEventMap = keyof HTMLElementEventMap> =
    { [P in K]: {
        eventName: P; callback: ContainedEventCallback<P>;
    } }[K];

function useContainedMultiplePhaseEvent(el: HTMLElement, events: ContainedEvent[]) {
    events.forEach(<K extends keyof HTMLElementEventMap>(e: ContainedEvent<K>) =>
        el.addEventListener(e.eventName, (ev) => e.callback(ev)));
}

其也如所期望的那样工作。您可以查看ms/TS#47109,了解分布式对象类型的工作原理;否则我就不离题在这里详细解释了。
Playground链接到代码

相关问题