I am trying to build an event-processing system for different models using TypeScript and have faced a super-weird error which looks like a bug of TypeScript.
Let's say we have a model ApprovalStep which is actually a union:
type ApprovalStepCompleted = {
state: 'completed',
};
type ApprovalStepBlocked = {
state: 'blocked',
};
type ApprovalStep = ApprovalStepCompleted | ApprovalStepBlocked;
We want to have specific handlers for every field of any arbitrary model. Here are the definition of event type and of a listeners set:
interface EventDao<Model> {
subject: Model;
}
type Listeners<Model> = Partial<{
[k in keyof Model]: (event: EventDao<Model>) => any;
}>;
This looks pretty straightforward, right? So we can just take any model (like ApprovalSteps) and build a map of listeners, like so:
const listeners: Listeners<ApprovalStep> = {
state: (ev: EventDao<ApprovalStep>) => {
console.log(ev);
}
}
But when we're trying to call a specific handler, we're getting super-strange error:
const exampleEvent: EventDao<ApprovalStep> = {
subject: {
state: 'completed',
}
}
if (listeners.state) {
// this one raises an error!
listeners.state(exampleEvent);
}
The error says:
Argument of type 'EventDao<ApprovalStep>' is not assignable to parameter of type 'EventDao<ApprovalStepCompleted> & EventDao<ApprovalStepBlocked>'.
Type 'EventDao<ApprovalStep>' is not assignable to type 'EventDao<ApprovalStepCompleted>'.
Type 'ApprovalStep' is not assignable to type 'ApprovalStepCompleted'.
Type 'ApprovalStepBlocked' is not assignable to type 'ApprovalStepCompleted'.
Types of property 'state' are incompatible.
Type '"blocked"' is not assignable to type '"completed"'.(2345)
Which is more strange is that when we manually use ApprovalStep for the Listeners type (instead of generic Model), it works!
type Listeners<Model> = Partial<{
[k in keyof ApprovalStep]: (event: EventDao<Model>) => any;
}>;
How this can make any difference? Model should 100% equal to ApprovalStep in this case!
The whole example can be found at typescript playground