请考虑以下类型:

export type ListenerToAll<Events> =
  <EventName extends keyof Events>(eventName: EventName, payload: Events[EventName]) => void

此操作失败

type CharacterEvents = {
    wait: {turns: number},
   speak: {message: string}
}

const liToAll: ListenerToAll<CharacterEvents> = (eventName, payload) => {
    if (eventName == 'wait') { // Narrowing
        // TS error: Property 'turns' does not exist on type 
        // '{ turns: number; } | { message: string; }'
        console.log(payload.turns)
    }
}

为什么无法推断类型?缩小范围还不够吗?这对我来说没有任何意义.

我try 了类型保护、switch 等.类型脚本可以理解该事件称为"等待",但它不能推断负载类型.

Link to TS Playground

推荐答案

目前(截至TS 5.1)control flow analysis不影响generic个类型参数.因此,在类型<K extends keyof CharacterEvents>(eventName: K, payload: CharacterEvents[K]) => void的函数中,判断eventName函数参数可以缩小其表观类型,但不会对类型参数K做任何操作.因此,payload的类型也不受影响,并且不推断eventNamepayload之间的相关性.他们被当作独立的人对待.

如果是eventName === "wait",这并不意味着K已经从K extends keyof CharacterEvents重新constrained变成K extends "wait".事实上,这样做在技术上是不正确的,因为K总是有可能是wider,而不是"等待":

liToAll(Math.random() > 0.01 ? "wait" : "speak", { message: "abc" }); // okay

这里K已经被推断为完整的union type keyof CharacterEvents.此外,eventName"wait"payload始终为{message: string}的可能性为99%,因此payload.turns在运行时未定义的可能性为99%.哎呀.看起来eventNamepayload实际上是aren't,保证以您想要的方式关联(尽管这种事情很可能不太可能,而且TypeScrip的其他部分也做出了技术上不正确但非常方便的假设).

这意味着您想要的行为在这里不涉及常规的泛型约束.相反,它将需要某种新的约束,因此这是语言中缺失的一个特征.问题microsoft/TypeScript#27808要求一个这样的功能,这样你就可以说K oneof keyof CharacterEvents这样的话,而不是K extends keyof CharacterEvents,这意味着K只能由keyof CharacterEvents中的一个成员指定.那么,也许勾选eventName === "wait"将允许将payload缩小到{turns: number}.

但就目前而言,它根本不是语言的一部分.


这就是问题的答案,但谈一谈另一种方法可能是相关的.目前,当您判断一个变量时,控制流分析将缩小另一个变量的范围的唯一方法是通过destructured discriminated unions.因此,如果eventNamepayload被认为是discriminated union类型的某个值的属性,它将开始工作.

区分联合需要文字类型的区分属性...幸运的是,eventName‘S类型是字符串类型"wait""speak".由于eventNamepayload是函数参数,因此可以将它们视为函数rest parameter01属性.也就是说,我们希望您的调用签名为以下非泛型类型:

 (...args: 
    [eventName: "wait", payload: { turns: number; }] | 
    [eventName: "speak", payload: { message: string; }]
 ) => void

我们可以重写ListenerToAll实用程序类型,以生成这样一个有区别的联合REST参数类型,如下所示:

export type ListenerToAll<E> =
  (...args: { [K in keyof E]:
    [eventName: K, payload: E[K]]
  }[keyof E]) => void;

这是一张distributive object type(在microsoft/TypeScript#47109中创造的).让我们来测试一下:

const liToAll: ListenerToAll<CharacterEvents> = (event, payload) => {
  if (event === 'wait') {
    console.log(payload.turns) // okay
  }
}

这是现在汇编的;万岁!之前打给Math.random()的电话现在被拒绝了:

liToAll(Math.random() > 0.01 ? "wait" : "speak", { message: "abc" }); // error

也太棒了!所以这大概是我能想象到的最接近你想要的行为了.元组联合作为REST参数类型的事情有点奇怪,但它是有效的.

最后,如果您希望在不使用liToAll类型的annotating的情况下编写此代码,则需要将实现的函数参数显式地作为REST参数编写,可能如下所示:

const liToAll = (...[event, payload]:
  [event: "wait", payload: { turns: number; }] |
  [event: "speak", payload: { message: string; }]
) => {
  if (event === 'wait') {
    console.log(payload.turns)
  }
}

Playground link to code

Typescript相关问答推荐

Angular -使用Phone Directive将值粘贴到控件以格式化值时验证器不工作

无法从应用程序内的库导入组件NX Expo React Native

TypeScript:在作为参数传递给另一个函数的记录中对函数的参数实现类型约束

在动态对话框中使用STEP组件实现布线

类型{}上不存在属性&STORE&A;-MobX&A;REACT&A;TYPE脚本

来自类型脚本中可选对象的关联联合类型

如何在另一个参数类型中获取一个参数字段的类型?

有条件地删除区分的联合类型中的属性的可选属性

在Reaction-Native-ReAnimated中不存在Animated.Value

CDK从可选的Lambda角色属性承担角色/复合主体中没有非空断言

如何将v-for中的迭代器编写为v-if中的字符串?

如何在方法中正确地传递来自TypeScrip对象的字段子集?

从子类集合实例化类,同时保持对Typescript中静态成员的访问

确保对象列表在编译时在类型脚本中具有唯一键

将对象的属性转换为正确类型和位置的函数参数

类型脚本-在调用期间解析函数参数类型

如何避免从引用<;字符串&>到引用<;字符串|未定义&>;的不安全赋值?

为什么特定的字符串不符合包括该字符串的枚举?

如何使用TypeScrip在EXPO路由链路组件中使用动态路由?

如何扩展RxJS?