我try 将generic类型赋给一个函数,这样它将返回我在generic类型中指定的类型. 第二个论点也应该从事件中寻找……

我想要做的是: listen<MenuChannels>('menu','1') // Should return type '3'

功能:

listen<T extends WebsocketChannel[]>(
    name: T[number]['channel'],
    event: T[number]['events'][number]['name'], // Doesnt work atm
  ): Observable<any> { // Doesnt work atm
}

类型:

export type MenuChannels = [
  {
    channel: 'menu'
    events: Menu1Events
  },
  {
    channel: 'menu-2'
    events: Menu2Events
  },
]

export type Menu1Events = [
  {
    name: '1'
    value: '3'
  },
  {
    name: '2'
    value: '4'
  },
]

export type Menu2Events = [
  {
    name: '5'
    value: '7'
  },
  {
    name: '6'
    value: '8'
  },
]
export interface WebsocketChannel {
  channel: string
  events: Event[]
}

interface Event {
  name: string
  value: any
}

推荐答案

您希望调用listen<MenuChannels>(⋯),并使返回类型仍然依赖于其输入类型(例如,listen<MenuChannels>('menu', '1')是类型"3",而listen<MenuChannels>('menu-2', '5')是类型"5"). 这意味着您希望listen<MenuChannels>仍然是generic,以便编译器可以为nameevent的输入推断出泛型类型参数K1K2,并使用它计算返回类型.

但你现在不能在TypeScript中这样做. TypeScript缺少microsoft/TypeScript#26242中讨论的所谓partial type argument inference.现在,如果你有一个泛型函数,比如listen(),你手动指定了一个类型参数,比如listen<MenuChannels>(⋯),那么你必须手动指定all个类型参数.你不能只指定T,然后让编译器推断K1K2.相反,如果你想要推断,你需要让编译器推断all个类型参数,包括T.类型参数推理要么全部要么没有.

在实现部分类型参数推理之前,您需要解决它.标准的解决方法是使用currying作为间接,以便listen<MenuChannels>()返回另一个泛型函数,该函数可以为您推断K1K2. 它将采用以下形式:

declare function listen<T extends WebsocketChannel[]>():
  <K1 extends ???, K2 extends ???>
    (name: K1, event: K2) => ???;

你可以这样称呼它:

listen<MenuChannels>()('menu', '1')

或者,如果你可能经常使用listen<MenuChannels>,你可以写道:

const listenMenuChannels = listen<MenuChannels>();
listenMenuChannels('menu', '1');    
listenMenuChannels('menu-2', '5');

如果没问题的话,我们可以继续.


为了使这一点更简单,将T extends WebsocketChannel[]转换为一个对象类型会很有帮助,在这个类型中,你可以将它与K1K2进行index into,以获得所需的返回类型. 我将编写一个名为WebsocketChannelLookup<T>的实用程序类型,目的是获得MenuChannels的以下输出:

type WebsocketChannelLookupMenuChannels =
  WebsocketChannelLookup<MenuChannels>;
/* type WebsocketChannelLookupMenuChannels = {
    menu: {
        1: "3";
        2: "4";
    };
    "menu-2": {
        5: "7";
        6: "8";
    };
} */

有了这个类型,你可以看到listen<MenuChannels>()('menu', '1')对应于WebsocketChannelLookupMenuChannels["menu"]["1"].

下面是实用程序类型实现:

type WebsocketChannelLookup<T extends WebsocketChannel[]> = {
  [U in T[number] as U['channel']]: {
    [E in U['events'][number] as E['name']]:
    E['value']
  }
}

这使用key remapping in mapped types来覆盖union个数组成员类型(T[number]T数组的元素类型,因为如果你索引到一个带有数字索引的数组中,你会得到一个元素). 我们使用channel成员作为键,对于值,我们类似地遍历events成员的元素以获得值.


有了这个,现在listen()看起来像:

declare function listen<T extends WebsocketChannel[]>():
  <K1 extends keyof WebsocketChannelLookup<T>, K2 extends keyof WebsocketChannelLookup<T>[K1]>
    (name: K1, event: K2) => WebsocketChannelLookup<T>[K1][K2];

所以我们取T,将其转换为WebsocketChannelLookup<T>,然后进行嵌套索引.

让我们来测试一下:

const listenMenuChannels = listen<MenuChannels>();
const x = listenMenuChannels('menu', '1');
//    ^? const x: "3"
const y = listenMenuChannels('menu-2', '5')
//    ^? const y: "7"

看上go 不错.这就是您想要的行为(只要您不介意由于缺少微软/TypeScrip#26242而需要额外的间接性).

Playground link to code

Typescript相关问答推荐

在TypeScript中,除了映射类型之外还使用的`in`二进制运算符?

如何访问Content UI的DatePicker/中的sx props of year picker?'<>

为什么TypeScript失go 了类型推断,默认为any?''

数组文字中对象的从键开始枚举

STypescript 件的标引签名与功能签名的区别

Select 下拉菜单的占位符不起作用

Angular 17子路由

`fetcher.load`是否等同于Get Submit?

TypeScrip:从对象中提取和处理特定类型的键

为什么我在这个重载函数中需要`as any`?

类型';字符串|数字';不可分配给类型';未定义';.类型';字符串';不可分配给类型';未定义';

如何编辑切片器以使用CRUD调用函数?

try 使Angular依赖注入工作

抽象类对派生类强制不同的构造函数签名

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

VUE 3类型';字符串';不可分配给类型';参考<;字符串&>

基于属性值的条件类型

TypeScript:如何使用泛型和子类型创建泛型默认函数?

JSX.Element';不可分配给类型';ReactNode';React功能HOC

递归地排除/省略两种类型之间的公共属性