I have a type that takes an optional generic. If generic G provided, a new property of type G must be included. However, I'm running into a problem when doing this in a function:
interface Message {
id: string;
message: string;
}
interface MessageWithContext<T> extends Message {
context: T;
};
// Optional generic. If generic provided,
// context must also provided.
type NotificationMessage<T = void> = T extends void
? Message
: MessageWithContext<T>;
interface Context1 {
status: 'GOOD';
}
interface Context2 {
status: 'BAD';
error: string;
}
type AllContext = Context1 | Context2;
// DOES NOT WORK. See error below.
function toMessage(context: AllContext): NotificationMessage<AllContext> {
return {
id: '123',
message: 'hello world',
context: context
};
}
The error that I'm getting:
Type '{ id: string; message: string; context: AllContext; }' is not assignable to type 'MessageWithContext<Context1> | MessageWithContext<Context2>'.
Type '{ id: string; message: string; context: AllContext; }' is not assignable to type 'MessageWithContext<Context2>'.
Types of property 'context' are incompatible.
Type 'AllContext' is not assignable to type 'Context2'.
Property 'error' is missing in type 'Context1' but required in type 'Context2'.(2322)
CodePudding user response:
This definition of NotificationMessage
:
type NotificationMessage<T = void> = T extends void
? Message
: MessageWithContext<T>;
is a distributive conditional type, because the type T
that you're checking is a generic type parameter. Distributive conditional types distribute the type operation over unions in the relevant type argument, so that NotificationMessage<A | B | C>
will be evaluated as NotificationMessage<A> | NotificationMessage<B> | NotificationMessage<C>
:
type M = NotificationMessage<AllContext>
// type M = MessageWithContext<Context1> |
// MessageWithContext<Context2>
Which means NotificationMessage<AllContext>
is itself a union type. And while the value { id: '123', message: 'hello world', context: context }
could be considered assignable to such a union type, the compiler doesn't make any attempt to verify this. It only tries to do this if the target type is a discriminated union, and NotificationMessage<AllContext>
is not one (it has no discriminant property... no, subproperties don't count, see ms/TS#18758). So there's an error.
If you really wanted NotificationMessage<AllContext>
to itself be a union type, we could look at how to fix up toMessage()
to allow it to work. But you don't seem to actually be trying to do this. You'd presumably be happy if NotificationMessage<AllContext>
were a single object type with a union-typed context
property.
That means you don't want NotificationMessage<T>
to be distributive in T
. The easiest way to "shut off" distributivity is to wrap each side of the T extends U
condition in square brackets to make one-element tuples:
type NotificationMessage<T = void> = [T] extends [void]
? Message
: MessageWithContext<T>;
See this Stack Overflow Q/A about avoiding distributivity for more information about why this works. Now you've got a non-distributive type:
type M = NotificationMessage<AllContext>
// type M = MessageWithContext<AllContext>
And now everything just works:
function toMessage(context: AllContext): NotificationMessage<AllContext> {
return {
id: '123',
message: 'hello world',
context: context
}; // okay
}