首页 > 解决方案 > 尝试从联合中提取类型时出现类型错误

问题描述

我正在尝试将类型定义添加到一些使用通过网络工作者传递消息的代码中。分派的消息有一个字符串类型的成员,可以在运行时使用它来区分它们。

// One type of Message
export interface IOpenFileWMsg {
    type: "OpenFileWMsg"
}

// Another type of message
export interface ICreateFileWMsg {
    type: "CreateFileWMsg"
}

// Disjoint union containing all types of messages
export type IWMsg = IOpenFileWMsg | ICreateFileWMsg

// Helper type to extract a type given its type identifier:
// Eg. ISpecializedWMsg<"OpenFileWMsg"> == OpenFileWMsg
//
// [R1]
export type ISpecializedWMsg<T extends IWMsg["type"]> = Extract<
    IWMsg,
    { type: T }
>

// Function which can handle a message of a specific type
// 
// [R2]
export interface IWMsgHandler<T extends IWMsg["type"]> {
    (msg: ISpecializedWMsg<T>): void
}

// Type of a dictionary of all handlers
export type IWMsgHandlers = {
    [K in IWMsg["type"]]: IWMsgHandler<K>
}

const handlers: IWMsgHandlers = {
    OpenFileWMsg(_event: IOpenFileWMsg) {},
    CreateFileWMsg(_event: ICreateFileWMsg) {},
}

// Handle a general message:
const handleMessage = <T extends IWMsg["type"]>(msg: ISpecializedWMsg<T>) => {
    const type: T = msg.type
    // [R3]
    const handler: IWMsgHandler<T> = handlers[type]
    handler(msg)
}

上面的 [R3] 行给了我以下错误:

Type 'IWMsgHandlers[T]' is not assignable to type 'IWMsgHandler<T>'.
  Type 'IWMsgHandler<"OpenFileWMsg"> | IWMsgHandler<"CreateFileWMsg">' is not assignable to type 'IWMsgHandler<T>'.
    Type 'IWMsgHandler<"OpenFileWMsg">' is not assignable to type 'IWMsgHandler<T>'.
      Types of parameters 'msg' and 'msg' are incompatible.
        Type 'Extract<IOpenFileWMsg, { type: T; }> | Extract<ICreateFileWMsg, { type: T; }>' is not assignable to type 'IOpenFileWMsg'.
          Type 'Extract<ICreateFileWMsg, { type: T; }>' is not assignable to type 'IOpenFileWMsg'.
            Type '{ type: T; } & ICreateFileWMsg' is not assignable to type 'IOpenFileWMsg'.
              Types of property 'type' are incompatible.
                Type 'T & "CreateFileWMsg"' is not assignable to type '"OpenFileWMsg"'.
                  Type '"CreateFileWMsg"' is not assignable to type '"OpenFileWMsg"'. [2322]

我想了解为什么这不起作用,以及这是否是 TypeScript 类型系统的已知限制。


到目前为止,我能做到的最好的是:

const handler = handlers[type] as IWMsgHandler<any>

在这种情况下,这种向上转换不会导致类型安全的损失,但我仍然很好奇为什么原来的方法不起作用。

标签: typescript

解决方案


当你说

const handleMessage = <T extends IWMsg["type"]>

扩展时IWMsg变得简单

const handleMessage = <T extends 'OpenFileWMsg' | 'CreateFileWMsg'>

你的意思是T可以是'OpenFileWMsg'or 'CreateFileWMsg'

但这不是编译器解释它的方式。对于编译器,它的字面意思是any type that extends either 'OpenFileWMsg' or 'CreateFileWMsg'

你可能会认为这与 just 没有什么不同'OpenFileWMsg' | 'CreateFileWMsg',但在 javascript 中,字符串是对象,类型系统必须对其进行正确建模,假设字符串字面量类型'OpenFileWMsg'确实可以扩展。

顺便说一下,这是如何扩展它的:

type X = 'OpenFileWMsg' & { foo: string };

let x: X = Object.assign('OpenFileWMsg', { foo: 'bar' });
const type: IWMsg['type'] = x;

所以,在这个实现中

const handleMessage = <T extends IWMsg["type"]>(msg: ISpecializedWMsg<T>) => {
    const type: T = msg.type
    // [R3]
    const handler: IWMsgHandler<T> = handlers[type]
    handler(msg)
}

不能保证handers[type]可以分配给,IWMsgHandler<T>因为T允许是任何扩展的类型IWMsg["type"],而不仅仅是联合的单个成员。

您必须使用类型断言:

const handleMessage = <T extends IWMsg["type"]>(msg: ISpecializedWMsg<T>) => {
    const type: T = msg.type
    // [R3]
    const handler = handlers[type] as IWMsgHandler<T>;
    handler(msg)
}

我记得在 TypeScript github 上看到一个功能请求或错误,有人要求能够表达一种类型必须恰好是联合的一个成员,但我现在找不到。

请注意,此答案解释了您将在--strictFunctionTypes关闭时遇到的错误。开启时错误会有所不同--strictFunctionTypes,因为编译器会遇到另一个不兼容问题,请参阅 Titian 的回答来解释这一点。


推荐阅读