Reputation: 4532
I am trying to automatically infer the type of some data passed in a switch
statement based on the enum
value fulfilling the case
condition.
To achieve this I've defined a const enum:
const enum MESSAGES {
open = 1,
close,
redo
}
Then I use it to index a Interface
:
interface MessagePayloadContent {
[MESSAGES.open]: string;
[MESSAGES.close]: number;
[MESSAGES.redo]: boolean;
}
At this point I define the object that I will evaluate in the switch
statement:
interface MessagePayload<T extends MESSAGES> {
scope: T;
content: MessagePayloadContent[T];
}
Finally I use the above in a switch statement.
I would expect that the interpreter can infer, based on the value being passed to each case
, what type of data will be contained in the object
.
On the contrary I get the errors as in the comments in the code below:
function pick(payload: MessagePayload<MESSAGES>): void {
switch (payload.scope) {
case MESSAGES.open:
open(payload.content); // Argument of type 'string | number | boolean' is not assignable to parameter of type 'string'.
break;
case MESSAGES.close:
close(payload.content as number); // This is my current workaround.
break;
case MESSAGES.redo:
redo(payload.content); // Argument of type 'string | number | boolean' is not assignable to parameter of type 'boolean'.
break;
}
}
const open = (d: string) => d;
const close = (d: number) => d;
const redo = (d: MessagePayloadContent[MESSAGES.redo]) => d;
What I don't fully get is that in some other circumstances this does work, so I wonder why some other times it does not.
Upvotes: 1
Views: 471
Reputation: 329608
The problem here is that the type MessagePayload<MESSAGES>
is not what you think it is. Interfaces don't generally distribute over unions, so it just evaluates to:
interface OopsMessagePayload {
scope: MESSAGES;
content: string | number | boolean;
}
Which means that if you check the scope
property, it does not narrow the type of the content
property.
TypeScript does have some type-level constructs that distribute over unions, so there should be a way to define
type MessagePayloadDistributive<T> = ...
such that
MessagePayloadDistributive<MESSAGES>
evaluates to
MessagePayload<MESSAGES.open> |
MessagePayload<MESSAGES.closed> |
MessagePayload<MESSAGES.redo>
I will use distributive conditional types in which if you have a type like type D<T> = T extends U ? V : W
where the checked type T
is a bare type parameter, the conditional check will be distributed across unions:
type MessagePayloadDistributive<T extends MESSAGES> = T extends any
? MessagePayload<T>
: never;
type SomeMessagePayload = MessagePayloadDistributive<MESSAGES>;
If you inspect that, SomeMessagePayload
evaluates to the desired type above. And then the following code will work as you expect:
function pick(payload: SomeMessagePayload): void {
switch (payload.scope) {
case MESSAGES.open:
open(payload.content); // okay
break;
case MESSAGES.close:
close(payload.content); // okay
break;
case MESSAGES.redo:
redo(payload.content); // okay
break;
}
}
Okay, hope that helps. Good luck!
Upvotes: 2