Higher order generics in TypeScript do not fully support representing Parameters<TrackEvent> without losing information. If TypeScript included existentially quantified generic types as requested in microsoft/TypeScript#14466, then Parameters<TrackEvent> could potentially be defined differently.
type TrackEventParams = Parameters<TrackEvent>;
// This syntax is invalid and should not be used
/* type TrackEventParams =
<∃K extends keyof Events, ∃E extends keyof Events[K], ∃P extends Events[K][E]>[
tag: keyof Events, name: never, opts: never
]
*/
Existential generic types are currently not supported in TypeScript, so the utility of Parameters<T> utilizes conditional types with constraints when matching against a generic function.
By specifying constraints like K extends keyof Events, E extends keyof Events[K], P extends Events[K][E], Parameters<TrackEvent> behaves as if TrackEvent were initially defined like:
type K = keyof Events;
type E = keyof Events[K];
type P = Events[K][E];
type TrackEvent = (tag: K, name: E, opts: P) => void;
// type TrackEvent = (tag: keyof Events, name: never, opts: never) => void
The reason why E is 'never' in this case is due to the lack of common property names between Events["car"] and Events["plane"], leading to only key names shared among all members of the union being returned by keyof.
In cases where existentially quantified generics are necessary but no specific type equivalent exists in TypeScript, existential types can be thought of as the union of the type for all possible instantiations of the generic. Finite unions, which exist in TypeScript, can accurately represent constrained types like K extends keyof Events.
To create a finite union programmatically using distributive object types, you can define TrackEventParams as demonstrated below:
type TrackEventParams = { [K in keyof Events]:
{ [E in keyof Events[K]]:
[tag: K, name: E, opts: Events[K][E]]
}[keyof Events[K]]
}[keyof Events];
/* type TrackEventParams =
[tag: "car", name: "drive", opts: { city: string; }] |
[tag: "plane", name: "fly", opts: { country: string; }] */
This specific type can be used instead of Parameters<TrackEvent>. Additionally, TrackEvent can be redefined in terms of TrackEventParams depending on the use case.
For more details and examples, see the provided Playground link