Typescript 尝试从联合中提取类型时出现类型错误

Typescript 尝试从联合中提取类型时出现类型错误,typescript,Typescript,我正在尝试将类型定义添加到一些使用通过web workers传递消息的代码中。调度的消息有一个字符串类型的成员,使用该成员可以在运行时对其进行区分 // One type of Message export interface IOpenFileWMsg { type: "OpenFileWMsg" } // Another type of message export interface ICreateFileWMsg { type: "CreateFileWMsg" }

我正在尝试将类型定义添加到一些使用通过web workers传递消息的代码中。调度的消息有一个字符串类型的成员,使用该成员可以在运行时对其进行区分

// 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>
在这种情况下,这种向上转换不会导致类型安全性的损失,但我仍然很好奇为什么原来的方法不起作用

当你说

const handleMessage = <T extends IWMsg["type"]>
您的意思是T可以是“OpenFileWMsg”或“CreateFileWMsg”

但编译器不是这样解释的。对于编译器来说,它字面上是指扩展“OpenFileWMsg”或“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)
}
我记得在TypeScript github上看到一个功能请求或bug,有人要求能够表示一个类型必须恰好是一个联合体的一个成员,但我现在找不到它


注意:此答案解释了关闭-strictFunctionTypes时将出现的错误。当-strictFunctionTypes打开时,错误会有所不同,因为编译器将遇到另一个不兼容问题,请参阅Titian的答案解释。

因为编译器无法知道函数中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

不确定我是否真的同意您的解释,根据您的推理,这不应该起作用,但它确实起作用了:类型fns=fn1 | fn2 let test={fn1:s:number=>{},fn2:s:number=>{}}函数带有test k:T{let testFn:s:number=>void=test[k];testFn0}实际上是“OpenFileWMsg”&{foo:string}将可用作键并产生正确的结果,派生类型在需要基类型时可用。在操场上,错误消息中的最后一行是类型“OpenFileWMsg”不可分配给类型“T”。。我试图解释为什么它是不可分配的。请注意,这是在strictFunctionTypes已关闭的情况下进行的。抱歉,您是对的,în non strick解释是正确的。我的否决票被锁定了,如果你修改答案,我可以重新公布。我的错
const handleMessage = <T extends IWMsg["type"]>(msg: ISpecializedWMsg<T>) => {
    const type: T = msg.type
    // [R3]
    const handler = handlers[type] as IWMsgHandler<T>;
    handler(msg)
}
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
function takesNumber(n: number){}
function takesSting(s: string){}

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