It appears that the main issue at hand lies in how TypeScript deduces a generic element type from an array literal. The inference process only takes into account the first element of the array, which is usually desirable as it enforces homogeneity. This means that a function like foo<T>(...args: T[]) {}
can accept inputs like
foo("a", "b", "c")
and
foo(1, 2, 3)
, but not
foo("a", 2, "c")
. In your scenario, however, you require a heterogeneous array.
To address this, the approach typically involves modifying the generic type parameter to encompass the entire array rather than just the element type. In your case, adapting K
to reference the tuple of type arguments for a tuple of ContainedEvent
s could resolve the issue. This would mean defining K
as something like
["pointerdown", "pointermove"]
and specifying that
events
should be of type
[ContainedEvent<"pointerdown">, ContainedEvent<"pointermove">]
.
Consider this implementation:
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));
}
}
In this function, the type of events
becomes a mapped tuple type where each element indexed by I
references K[I]
wrapped with ContainedEvent
, denoted as ContainedEvent<K[I]>
. Additionally, the type of events
is enclosed in a variadic tuple type [...⋯]
to instruct the compiler to infer its type as a tuple instead of an unordered array.
Testing this solution:
useContainedMultiplePhaseEvent(div, [
{ eventName: "pointerdown", callback: doA, },
{ eventName: "pointermove", callback: doB, }
]); // successful
// useContainedMultiplePhaseEvent<["pointerdown", "pointermove"]>
The results are promising!
This response addresses the original query point raised.
While alternative approaches exist, I opted to stay close to your initial code structure. Since K
in ContainedEvent<K>
primarily pertains to a fixed union keyof HTMLElementEventMap
, transforming ContainedEvent
itself into a union might be feasible. This could involve altering the definition to a distributive object type following guidelines from ms/TS#47109. Consequently, the useContainedMultiplePhaseEvent
function may no longer need to be generic, as each element within events
would simply adhere to the union type 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)));
}
This revised version also delivers the intended functionality. For further insights into how a distributive object type operates, refer to ms/TS#47109 or relevant resources available online.
Playground link to code