One issue with TypeScript is the lack of control flow analysis on generic type parameters. This means that when you perform a check like eventName == 'ageChanged'
, although the type of the eventName
variable may be narrowed, it doesn't affect the type K
. Even after the check, K extends keyof User
remains and does not change. Therefore, the callback function (e: User[K]) => void
cannot be guaranteed to be of type (e: number) => void
, resulting in an error:
on(eventName, callback) {
if (eventName == 'ageChanged') {
callback(133) // error
}
}
There have been numerous requests on GitHub for enhancing TypeScript to allow narrowing of generic type parameters based on control flow. Check out issues like microsoft/TypeScript#24085 and microsoft/TypeScript#27808 for more details. While waiting for such enhancements, one workaround is to use a type assertion to suppress the error:
on(eventName, callback) {
if (eventName == 'ageChanged') {
(callback as (e: number) => void)(133) // okay
}
}
However, this approach sacrifices type safety.
An alternative to generics is to introduce rest parameters with a union of tuples in your on()
function. By listing all possible combinations of eventName
/callback
pairs in a discriminated union, you can ensure proper narrowing based on the event name:
type EventWatchParams =
["firstnameChanged", (e: string) => void] |
["lastnameChanged", (e: string) => void] |
["ageChanged", (e: number) => void] |
["onChanged", (e: EventWatch) => void];
type EventWatch = {
(...args: EventWatchParams): void;
};
To automatically generate these pairs based on a type like User
, you can utilize distributive object types along with indexed access into mapped types:
type EventWatchParams = {
[K in keyof User]: [`${K}Changed`, (e: User[K]) => void]
}[keyof User]
This definition ensures equivalence to the earlier union setup.
In terms of implementation, prior to TypeScript 4.6, handling rest parameters might involve accessing elements by index. With TypeScript 4.5 and below:
on(...args) {
if (args[0] == 'ageChanged') {
args[1](133) // okay
}
}
As TypeScript evolves, TypeScript 4.6 introduces destructuring support which improves readability while maintaining type safety:
// TS4.6+
on(...args) {
const [eventName, callback] = args;
if (eventName == 'ageChanged') {
callback(133) // okay
}
}
While this method offers good progress, further enhancements are desired such as using named parameters instead of spread arguments. Such advancements are still pending resolution in issues like microsoft/TypeScript#46680.
Check out Playground link to code for demonstration.