Assume the Contact Interface and the ContactDeletedEvent and ContactStatusChangedEvent Interfaces that represent events of the Contact type, each event defined with its own set of properties.
Having grouped the events under a single Interface called ContactEvents, I wrote a function to handle the different event types based on the property name of ContactEvents that describes the event type.
Instead of detecting which properties are appropriate for the "deleted" event, TypeScript throws an error that all the properties for a union type of ContactDeletedEvent & ContactStatusChangedEvent are required.
Here is the .ts code
type ContactStatus = "active" | "inactive" | "new";
interface Contact {
id: number;
name: string;
status: ContactStatus;
address: string;
}
interface ContactEvent {
contactId: Contact["id"];
}
interface ContactDeletedEvent extends ContactEvent {}
interface ContactStatusChangedEvent extends ContactEvent {
oldStatus: Contact["status"];
newStatus: Contact["status"];
}
interface ContactEvents {
deleted: ContactDeletedEvent;
statusChanged: ContactStatusChangedEvent;
// ... and so on
}
function handleEvent<T extends keyof ContactEvents>(
eventName: T,
handler: (evt: ContactEvents[T]) => void
) {
if (eventName === "deleted") {
handler({ contactId: 1 });
} else {
handler({ contactId: 1, oldStatus: "active", newStatus: "inactive" });
}
}
handleEvent("statusChanged", (evt) => evt);
The output of the tsc:
src/demo.ts:44:13 - error TS2345: Argument of type '{ contactId: number; }' is not assignable to parameter of type 'ContactEvents[T]'.
Type '{ contactId: number; }' is not assignable to type 'ContactDeletedEvent & ContactStatusChangedEvent'.
Type '{ contactId: number; }' is missing the following properties from type 'ContactStatusChangedEvent': oldStatus, newStatus
44 handler({ contactId: 1 });
~~~~~~~~~~~~~~~~
CodePudding user response:
TypeScript does not do any narrowing on generic types. Even after checking if eventName
is "deleted"
, both eventName
and handler
are still unions which leads to the error when trying to call handler
.
As a work-around, I would propose to use a function overload where the implementation function is not generic. The implementation will use a union of all valid eventName
and handler
combinations as the parameter.
We can generate those combinations like this:
type HandleEventParams = {
[K in keyof ContactEvents]: [
eventName: K,
handler: (evt: ContactEvents[K]) => void
]
}[keyof ContactEvents]
Now we need to overload the function.
function handleEvent<T extends keyof ContactEvents>(
eventName: T,
handler: (evt: ContactEvents[T]) => void
): void
function handleEvent(
...args: HandleEventParams
) {
const [eventName, handler] = args
if (eventName === "deleted") {
handler({ contactId: 1 });
} else {
handler({ contactId: 1, oldStatus: "active", newStatus: "inactive" });
}
}
This removes the errors and gives us a type-safe implementation.
The function will still be called as before.
handleEvent("statusChanged", (evt) => evt);