网站搜索

可选泛型中的 Typescript 联合类型无法正常工作


我有一个接受可选泛型的类型。如果提供了泛型 G,则必须包含一个类型为 G 的新属性。然而,当我在函数中这样做时遇到了一个问题:

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
  };
}

游乐场链接。

我收到的错误:

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)

答案#1

这个 NotificationMessage 的定义:

type NotificationMessage<T = void> = T extends void
  ? Message
  : MessageWithContext<T>;

是一种分配条件类型,因为你正在检查的类型T是一个泛型类型参数。分配条件类型会将类型操作分配到相关类型参数中的联合类型上,因此NotificationMessage<A | B | C>将被评估为NotificationMessage<A> | NotificationMessage<B> | NotificationMessage<C>:

type M = NotificationMessage<AllContext>
// type M = MessageWithContext<Context1> | 
//   MessageWithContext<Context2>

这意味着 NotificationMessage<AllContext> 本身是一个联合类型。虽然值 { id: '123', message: 'hello world', context: context } 可能 被视为可分配给这样的联合类型,但编译器不会尝试去验证这一点。它只会在目标类型是判别联合类型时才会进行此操作,而 NotificationMessage<AllContext> 并不是(它没有判别属性……不,子属性不算,参见 ms/TS#18758)。所以会出现错误。

如果你真的希望 NotificationMessage<AllContext> 本身是一个联合类型,我们可以研究如何修复 toMessage() 以使其能够工作。但你似乎实际上并没有尝试这样做。如果 NotificationMessage<AllContext> 是一个具有联合类型 context 属性的单一对象类型,你大概会感到满意。

这意味着你不希望 NotificationMessage<T> 在 T 中具有分配性。关闭分配性的最简单方法是将 T extends U 条件的两边用方括号括起来,形成单元素元组:

type NotificationMessage<T = void> = [T] extends [void]
  ? Message
  : MessageWithContext<T>;

查看这个关于避免分配律的 Stack Overflow 问答,以了解为什么这有效。现在你已经有了一个非分配类型:

type M = NotificationMessage<AllContext>
// type M = MessageWithContext<AllContext>

而现在一切都正常工作了:

function toMessage(context: AllContext): NotificationMessage<AllContext> {
  return {
    id: '123',
    message: 'hello world',
    context: context
  }; // okay
}

代码的 Playground 链接