尝试从联合中提取类型时发生类型错误

时间:2018-07-07 22:23:43

标签: typescript

我试图将类型定义添加到一些使用通过Web Worker传递消息的代码中。调度的消息具有字符串类型的成员,可以在运行时使用它们来区分它们。

// 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>

在这种情况下,这种向上转换不会导致类型安全性损失,但是我仍然很好奇为什么原始方法无效。

2 个答案:

答案 0 :(得分:2)

当你说

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

IWMsg展开时,它变得简单

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

您的意思是T可以是'OpenFileWMsg''CreateFileWMsg'

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

您可能会认为这与'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的答案对此进行解释。

答案 1 :(得分:2)

由于编译器无法了解函数内部的T的特定类型,因此T基本上等效于两个可能的值"OpenFileWMsg" | "CreateFileWMsg"(以及从它们派生的任何值),如果扩展双方得到的类型:

typeof handlers[type] 
    = typeof handlers[IWMsg["type"]]; 
    = IWMsgHandler<"OpenFileWMsg"> | IWMsgHandler<"CreateFileWMsg">
    = ((msg: IOpenFileWMsg) => void) | ((msg: ICreateFileWMsg) => void)

IWMsgHandler<IWMsg["type"]> 
    = IWMsgHandler<"OpenFileWMsg" | "CreateFileWMsg">
    = (msg: IOpenFileWMsg | ICreateFileWMsg) => void

因此,基本上,您是将可以采用IOpenFileWMsg的函数或可以采用ICreateFileWMsg的函数分配给可以采用IOpenFileWMsgICreateFileWMsg的函数< / p>

一个为什么不安全的简化示例是:

function takesNumber(n: number){}
function takesSting(s: string){}

let takesEither : (sn: number| string)=> void = takesNumber; // Invalid
takesEither("") // This would be a runtime error