TypeScript使用泛型回调处理不同事件类型时的类型错误。

huangapple go评论59阅读模式
英文:

TypeScript type error when using generic callbacks for different event types

问题

我正在尝试创建一种通用事件处理程序,我想能够指定事件键,比如 "pointermove" 并让 TypeScript 推断事件类型在这种情况下为 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,
    },
]);
英文:

I'm trying to make some kind of a generic event handler, I want to be able to specify the event key like "pointermove" and typescript to infer the event type in this case PointerEvent, but I get an error when using more than one event.

here's a minimal repoducible example

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

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

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

答案1

得分: 1

以下是翻译好的部分:

我认为这里的主要问题是,当TypeScript从数组文字中推断出泛型 元素 类型时,它只会查看数组的第一个元素。这通常是人们想要的,因为它允许像function foo<T>(...args: T[]) {}这样的东西接受foo("a", "b", "c")foo(1, 2, 3),但拒绝foo("a", 2, "c")。也就是说,当从类型为T[]的值中推断T时,编译器只会推断同质数组。但在您的情况下,您希望允许异质数组。

在这种情况下的常规方法是更改泛型类型参数,使其引用整个数组,而不仅仅是数组的元素类型。在您的情况下,这意味着不仅要使K引用ContainedEvent的类型参数,还要使其引用ContainedEvent的类型参数的元组类型。也就是说,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的类型是一个映射元组类型,其中K的每个元素在数字索引I处,类型为K[I],被包装在ContainedEvent中以获得ContainedEvent<K[I]>。噢,events的类型还包装在可变元组类型 [...⋯]中,以向编译器提示我们希望events的类型被推断为元组而不是无序数组(此行为在ms/TS#39094中描述)。

让我们试一试:

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

看起来不错!

这回答了您提出的问题。

还有其他可能的方法,但我不想偏离您的原始代码太远。由于ContainedEvent<K>中的K实际上只能是固定的联合类型keyof HTMLElementEventMap之一,您实际上可以将ContainedEvent本身变成一个联合类型,可能通过将定义更改为描述分布式对象类型(在ms/TS#47109中描述)。然后,您的useContainedMultiplePhaseEvent函数将不需要是泛型的,因为events的每个元素将只是联合类型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((e: ContainedEvent) =>
        el.addEventListener(e.eventName, (ev) => e.callback(ev)));
}

这也能正常工作。您可以查看ms/TS#47109以了解分布式对象类型的工作原理;否则,我不会在这里偏离主题进行详细说明。

[Playground链接到代码](https://www.typescriptlang.org/play?#code/FAOwhgtgpgzgDmAxlABAWTHOUAmBBAJwLAE8UBvYYFGlAFxOxQGEB7EOsASxFwFEAblA4AeANIooADzrCcMFAGsoJVgDMUACQAqaADJ8ANlGgdBwuhjgA+FAF4K1Ws6hCOAOUhQAXCjEBuJ2caRDBDQwAjJEVfNg5uXhxzDmYwyOjxa0DnAF9s2gYmOM4efjc6VPCoxEVxSRk5BWVVDR19IxMLZMtMWwcAClcLXzaDY1M6bqsAbTEAXQBKe1sBVi4cfJo1AFcQRDoudhRtmChihNw0bcMDuGMABQALMFPuuulZEHkUAigwHHYhjI-Wa6i0ujGnTM5SsC2mc2s-SCwSghhGEI6EwANJJyjBfNMAHTE8goaYASRQPCUKjB81i7BKiTeYgpCJQOTmWORS0owVoalYBBQ-UQ7BgdEkKDBQw4MF5yP5NFRhP+SXKei4EuEUAIg0JsronmgOMGAiWdlsUEJoSq0TNCwWm2COWRruRYpAEpQOC4AnsPtYiG2ExtvzAskxFn6ACJfQIY06PeLJQC8Az4qV1RZKukaiIY3A1hxdQCAO4gGN9EW

英文:

I think the primary issue here is that when TypeScript infers a generic element type from an array literal, it only consults the first element of the array. This is often what people want, since since it lets something like function foo&lt;T&gt;(...args: T[]) {} accept foo(&quot;a&quot;, &quot;b&quot;, &quot;c&quot;) and foo(1, 2, 3), but reject foo(&quot;a&quot;, 2, &quot;c&quot;). That is, when inferring T from a value of type T[], the compiler will only infer homogeneous arrays. But in your case you want to allow a hetereogenous array.

The normal approach in such cases is to change the generic type parameter so that it refers to the entire array and not just the element type of the array. In your case that would mean instead of having K refer to the type argument for ContainedEvent, you could make it refer to the tuple of type arguments for a tuple of ContainedEvents. Meaning K would be something like [&quot;pointerdown&quot;, &quot;pointermove&quot;] and events would be of type [ContainedEvent&lt;&quot;pointerdown&quot;&gt;, ContainedEvent&lt;&quot;pointermove&quot;&gt;].

Like this:

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

so the type of events is a mapped tuple type where each element of K at numeric-like index I, of type K[I], is wrapped with ContainedEvent to get ContainedEvent&lt;K[I]&gt;. Oh and the type of events is wrapped in a variadic tuple type [...⋯] to give the compiler a hint that we want events's type to be inferred as a tuple instead of an unordered array (this behavior is described in ms/TS#39094).

Let's try it out:

useContainedMultiplePhaseEvent(div, [
    { eventName: &quot;pointerdown&quot;, callback: doA, },
    { eventName: &quot;pointermove&quot;, callback: doB, }
]); // okay
// useContainedMultiplePhaseEvent&lt;[&quot;pointerdown&quot;, &quot;pointermove&quot;]&gt;

Looks good!


That answers the question as asked.

There are other possible approaches but I didn't want to stray too far from your original code. Since K in ContainedEvent&lt;K&gt; can only really be one of a fixed union keyof HTMLElementEventMap, you could actually make ContainedEvent into a union itself, possibly by changing the definition to a distributive object type as described in ms/TS#47109. Then your useContainedMultiplePhaseEvent function wouldn't need to be generic, since each element of events would just be of the union type ContainedEvent. That looks like this:

type ContainedEvent&lt;K extends keyof HTMLElementEventMap = keyof HTMLElementEventMap&gt; =
    { [P in K]: {
        eventName: P; callback: ContainedEventCallback&lt;P&gt;;
    } }[K];

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

which also works as desired. You can look at ms/TS#47109 to see how a distributive object type works; otherwise I won't digress here with a detailed explanation.

Playground link to code

huangapple
  • 本文由 发表于 2023年6月6日 03:33:27
  • 转载请务必保留本文链接:https://go.coder-hub.com/76409502.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定