To easily accomplish this task, consider aggregating a union of all relevant Event
types with unique string literal types for their type
properties. This union can be utilized to differentiate the union. Define the union as follows:
type Events = TodoAddedEvent | TodoRemovedEvent;
Next, describe the union of all possible
EventTypeAndHandlerTuple<T>
types for each element
T
in the
Events
union using techniques like
distributive conditional types:
type UnionOfEventTypeAndHandlerTuples = Events extends infer T ?
T extends Event ? EventTypeAndHandlerTuple<T> : never : never;
// type UnionOfEventTypeAndHandlerTuples =
// EventTypeAndHandlerTuple<TodoAddedEvent> | EventTypeAndHandlerTuple<TodoRemovedEvent>
The desired type is now simply
UnionOfEventTypeAndHandlerTuples[]
:
// valid
const eventTypeToHandlerTuples: UnionOfEventTypeAndHandlerTuples[] = [
[todoAddedEvent, handleTodoAddedEvent],
[todoRemovedEvent, handleTodoRemovedEvent]
];
// error
const badEventTypeToHandlerTuples: UnionOfEventTypeAndHandlerTuples[] = [
[todoRemovedEvent, handleTodoAddedEvent], // error!
];
If you don't have a pre-defined union, the process becomes more challenging. In the absence of native support for existential generic types in TypeScript (see microsoft/TypeScript#14466), it's not feasible to have a specific type like
EventTypeAndHandlerTuple<exists T extends Event>[]
, where
exists T
means "I'm indifferent about the exact nature of
T
, I just need it to
exist". You might consider existential types as an "infinite union" of compatible types.
In such scenarios, employing regular generic types and utilizing helper functions could be a viable approach in TypeScript:
const asEventTypeToHandlerTuples = <T extends Event[]>(
tuples: [...{ [I in keyof T]: EventTypeAndHandlerTuple<Extract<T[I], Event>> }]
) => tuples;
In this setup, the asEventTypeToHandlerTuples()
function receives an argument named
tuples</code, constraining its type to a <a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-1.html#mapped-types-on-tuples-and-arrays" rel="nofollow noreferrer">mapped tuple type</a>. It transforms an array of <code>Event
types into the corresponding array of
EventTypeAndHandlerTuple
types based on the inferred type of
T</code. By allowing the compiler to <em>infer</em> types rather than explicitly declaring them, the codebase remains cleaner:</p>
<pre><code>// valid
const eventTypeToHandlerTuples = asEventTypeToHandlerTuples([
[todoAddedEvent, handleTodoAddedEvent],
[todoRemovedEvent, handleTodoRemovedEvent]
]);
// error
const badEventTypeToHandlerTuples = asEventTypeToHandlerTuples([
[todoRemovedEvent, handleTodoAddedEvent], // error!
]);
While dealing with unions of particular types like Events
tends to be simpler than handling generic type parameters throughout your codebase, generic helper functions serve as effective alternatives when existential types are unattainable.
Click here for Playground link to code