
本文探讨了在typescript中为不同事件类型使用泛型回调时遇到的类型推断问题,特别是当数组包含多种泛型实例时,typescript默认的同构推断机制会导致类型错误。文章提供了两种主要解决方案:一是通过将泛型参数提升至整个数组元组层面,利用映射元组类型和可变参数元组类型来精确推断;二是通过将containedevent定义为分布式对象类型,使其本身成为一个联合类型,从而简化函数签名。
在TypeScript开发中,构建一个能够处理多种不同事件类型的通用事件处理器是一个常见的需求。我们可能希望定义一个结构,其中包含事件名称及其对应的回调函数,并让TypeScript能够根据事件名称自动推断出回调函数的事件参数类型。然而,当尝试在一个数组中混合使用不同事件类型的这种结构时,TypeScript的类型推断机制可能会导致类型错误。
考虑以下初始的类型定义和事件处理函数:
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", e.type); // e is PointerEvent
};
const doB: ContainedEventCallback<"pointermove"> = (e) => {
console.log("B", e.type); // e is PointerEvent
};
useContainedMultiplePhaseEvent(div, [
{ eventName: "pointerdown", callback: doA },
{ eventName: "pointermove", callback: doB }
]);在上述代码中,当我们尝试将一个包含 pointerdown 和 pointermove 两种不同事件类型的 ContainedEvent 对象数组传递给 useContainedMultiplePhaseEvent 函数时,TypeScript会报错。
这个问题的核心在于TypeScript对数组字面量的泛型推断行为。当TypeScript从一个数组字面量推断泛型元素类型时,它通常只参考数组的第一个元素。这意味着,如果一个泛型函数接收 T[] 类型的参数,并且我们传入 [value1, value2, ...],TypeScript会尝试为整个数组推断出一个单一的、同构的 T 类型。
在我们的例子中,events: ContainedEvent<K>[] 期望数组中的所有元素都具有相同的 K 类型。但我们传入的数组中,第一个元素的 K 是 "pointerdown",第二个元素的 K 是 "pointermove"。TypeScript会尝试找到一个能够同时满足 "pointerdown" 和 "pointermove" 的共同类型,这通常会导致 K 被推断为 keyof HTMLElementEventMap(即所有事件名称的联合类型),进而导致 ContainedEvent<K> 成为一个泛化类型,使得 doA 和 doB 的具体类型与泛化类型不匹配而报错。
为了解决这个问题,我们需要引导TypeScript以异构的方式推断数组的类型。
最直接的解决方案是改变泛型参数的范围,使其不再是数组元素的类型,而是整个数组(作为元组)的类型。这样,TypeScript就可以精确地推断出数组中每个元素的具体类型。
我们将 useContainedMultiplePhaseEvent 函数的泛型参数 K 定义为一个只读的事件名称元组,然后利用映射元组类型来构建 events 参数的类型。
export type ContainedEvent<K extends keyof HTMLElementEventMap> = {
eventName: K;
callback: ContainedEventCallback<K>;
};
export type ContainedEventCallback<K extends keyof HTMLElementEventMap> = (
event: HTMLElementEventMap[K],
) => void;
function useContainedMultiplePhaseEvent<
K extends readonly (keyof HTMLElementEventMap)[]
>(
el: HTMLElement,
// 使用映射元组类型和可变参数元组类型
events: [...{ [I in keyof K]: ContainedEvent<K[I]> }],
) {
for (const e of events) {
// e.eventName 和 e.callback 的类型现在是正确的
el.addEventListener(e.eventName, (ev) => (e as ContainedEvent<typeof e.eventName>).callback(ev));
}
}
const div = document.createElement("div");
const doA: ContainedEventCallback<"pointerdown"> = (e) => {
console.log("A", e.type);
};
const doB: ContainedEventCallback<"pointermove"> = (e) => {
console.log("B", e.type);
};
useContainedMultiplePhaseEvent(div, [
{ eventName: "pointerdown", callback: doA },
{ eventName: "pointermove", callback: doB }
]);
// 此时,useContainedMultiplePhaseEvent 的 K 被推断为 ["pointerdown", "pointermove"]
// events 被推断为 [ContainedEvent<"pointerdown">, ContainedEvent<"pointermove">]代码解析:
这种方法能够精确地保留每个事件对象的具体类型,使得 useContainedMultiplePhaseEvent 函数能够正确处理异构的事件数组。
另一种方法是重新定义 ContainedEvent 类型,使其本身成为一个联合类型(Union Type)。这种技术被称为分布式对象类型(Distributive Object Type),它利用了映射类型在联合类型上的分配特性。
export type ContainedEventCallback<K extends keyof HTMLElementEventMap> = (
event: HTMLElementEventMap[K],
) => void;
// 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)));
}
const div = document.createElement("div");
const doA: ContainedEventCallback<"pointerdown"> = (e) => {
console.log("A", e.type);
};
const doB: ContainedEventCallback<"pointermove"> = (e) => {
console.log("B", e.type);
};
useContainedMultiplePhaseEvent(div, [
{ eventName: "pointerdown", callback: doA },
{ eventName: "pointermove", callback: doB }
]);
// 此时,events 被推断为 ContainedEvent<"pointerdown"> | ContainedEvent<"pointermove"> 的数组代码解析:
这两种方法都能有效地解决TypeScript在处理异构泛型数组时的类型推断问题:
在实际开发中,选择哪种方案取决于具体的需求。如果你的事件处理器需要处理一个固定顺序、固定数量且类型各异的事件集合,方案一可能更合适。如果事件集合的顺序和数量不固定,只需要确保每个事件对象本身的类型是正确的,并且希望简化主函数的签名,那么方案二会是更简洁优雅的选择。
以上就是TypeScript中处理异构泛型回调的类型推断挑战与解决方案的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号