【问题标题】:How to make type inference work for a tuple with generic (on the example of array based queue)?如何使类型推断适用于具有泛型的元组(以基于数组的队列为例)?
【发布时间】:2021-04-29 22:10:30
【问题描述】:

假设我有一个分析对象的接口:

type Analytics = {
    identify: () => void;
    page: (title: string) => void;
    track: (eventName: string, props: object) => void;
}

我想实现某种队列,在我们能够执行它们之前收集所有动作。队列如下所示:

const queue = [
    ['identify'],
    ['page', 'some title'],
    ['track', 'event 1', { a: 'a' }],
    ['track', 'event 2', { b: 'b' }]
];

第一项始终是方法名称(identifypagetrack),其余的是来自 Analytics 类型的方法参数。

我想到的队列界面:

type QueueItem<T extends keyof Analytics> = [T, ...Parameters<Analytics[T]>]

好的,我们试试吧:

const item1: QueueItem = ['track', 'some event', {}]

因此,我收到一个 Typescript 错误:Generic type 'QueueItem' requires 1 type argument(s)

如果不能自动推断显式泛型类型怎么办?

const item2: QueueItem<keyof Analytics> = ['track'] // ???? no error, although "track" method expects to receive two parameters
const item3: QueueItem<keyof Analytics> = ['track2'] // ???? error as there is no such method in Analytics

没有按预期工作。 Typescript 认为项目的类型是 [keyof Analytics] | [keyof Analytics, string] | [keyof Analytics, string, object],它允许方法名称(identifypagetrack)和 ['identify', 'some string', {}] 等参数的不同组合。

但它可以与函数一起使用吗?它将:

function func<T extends keyof Analytics>(method: T, ...args: Parameters<Analytics[T]>) {}

func('track') // ???? error, because track has 2 parameters
func('track', 'event', {}) // ???? no error
func('identify') // ???? no error
func('page', 'title') // ???? no error
func('page') // ???? error as we need to pass title

应该对QueueItem 类型进行哪些更改以使其按预期工作(与函数相同)?

Typescript playground

【问题讨论】:

    标签: typescript tuples


    【解决方案1】:

    您的问题是当Tunion 时,QueueItem&lt;T&gt; 产生一个联合元组,而您想要 是一个联合元组。 tuple-of-unions 允许方法名和参数列表不匹配。

    在您的泛型函数中,编译器通常可以将 T 推断为来自 Analytics 的单个键,因此类型 Parameters&lt;Analytics[T]&gt; 正是您期望的类型。但是,在某些情况下,即使是通用函数也会将 T 推断为联合,然后会出现同样的问题:

    const method = (["identify", "page", "track"] as const)[
      Math.floor(Math.random() * 3)];
    // const method: "identify" | "page" | "track"
    func(method); // <-- no error, but there is a 67% chance of a problem at runtime
    

    正如我所说,您真的希望 QueueItem 成为元组的联合;您想分别考虑keyof Analytics 中的每个键K,为该键计算QueueItem&lt;K&gt;,然后合并所有结果。换句话说,您想分发 QueueItem&lt;K&gt;K 中的工会。一种方法是使用distributive conditional types:

    type QueueItem =
        keyof Analytics extends infer K ? K extends keyof Analytics ?
        [K, ...Parameters<Analytics[K]>]
        : never : never
    /* type QueueItem = ["identify"] | ["page", string] | ["track", string, object] */
    

    不幸的是,分布式条件类型实际上只分布在“裸”类型参数上,所以上面使用conditional type inferencekeyof Analytics 变成这样一个裸类型参数。

    我更喜欢在每个键 K in keyof Analytics 上使用 mapped type,然后使用 index into itkeyof Analytics 来获得所需的联合:

    type QueueItem = {
        [K in keyof Analytics]: [K, ...Parameters<Analytics[K]>]
    }[keyof Analytics]
    /* type QueueItem = ["identify"] | ["page", string] | ["track", string, object] */
    

    不管怎样都行。


    您可以验证QueueItem 的此类定义是否会产生您想要的行为:

    const item1: QueueItem = ['track', 'some event', {}] // okay
    const item2: QueueItem = ['track'] // error
    const item3: QueueItem = ['track2'] // error
    

    Playground link to code

    【讨论】:

    • 非常感谢您提供如此详细的回答。我什至会说一个迷你讲座?我也更喜欢第二种变体,看起来更简单
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-09-23
    • 2019-11-13
    • 2023-02-25
    • 1970-01-01
    • 2020-01-25
    • 1970-01-01
    • 2014-09-07
    相关资源
    最近更新 更多