简体   繁体   English

如何使类型推断适用于具有泛型的元组(以基于数组的队列为例)?

[英]How to make type inference work for a tuple with generic (on the example of array based queue)?

Imagine I have an interface for analytics object:想象一下,我有一个分析接口 object:

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

And I want to implement some kind of queue that will collect all actions before we will be able to execute them.我想实现某种队列,在我们能够执行它们之前收集所有动作。 The queue looks as follows:队列如下所示:

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

First item is always a method name ( identify , page , track ) and the rest are method arguments from Analytics type.第一项始终是方法名称( identifypagetrack ),并且 rest 是来自Analytics类型的方法 arguments 。

Interface for the queue that came into my mind:我想到的队列界面:

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

Ok, let's try it:好的,让我们尝试一下:

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

As a result I get a Typescript error: Generic type 'QueueItem' requires 1 type argument(s) .结果,我收到 Typescript 错误: Generic type 'QueueItem' requires 1 type argument(s)

What about explicit generic type if it can't be inferred automatically?如果不能自动推断显式泛型类型怎么办?

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

Not working as expected.没有按预期工作。 Typescript thinks that type of items is [keyof Analytics] | [keyof Analytics, string] | [keyof Analytics, string, object] Typescript 认为项目类型是[keyof Analytics] | [keyof Analytics, string] | [keyof Analytics, string, object] [keyof Analytics] | [keyof Analytics, string] | [keyof Analytics, string, object] [keyof Analytics] | [keyof Analytics, string] | [keyof Analytics, string, object] and it allows different combinations of method names ( identify , page , track ) and arguments like ['identify', 'some string', {}] . [keyof Analytics] | [keyof Analytics, string] | [keyof Analytics, string, object]并且它允许方法名称( identify , page , track )和 arguments 的不同组合,例如['identify', 'some string', {}]

But will it work with a function?但它可以与 function 一起使用吗? It will:它会:

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

What should be changed in QueueItem type to make it work as expected (the same way as with function)?应该对QueueItem类型进行哪些更改以使其按预期工作(与函数相同)?

Typescript playground Typescript 操场

Your problem is when T is a union , QueueItem<T> produces a tuple-of-unions, when what you want is a union-of-tuples.你的问题是当T是一个union时, QueueItem<T>产生一个联合元组,而你想要的是一个联合元组。 The tuple-of-unions allows mismatches between the method name and the parameter list. tuple-of-unions 允许方法名和参数列表不匹配。

In your generic function, the compiler can generally infer T to be a single key from Analytics and therefore the type Parameters<Analytics[T]> is exactly the type you expect.在您的通用 function 中,编译器通常可以将T推断为Analytics中的单个键,因此类型Parameters<Analytics[T]>正是您期望的类型。 There are situations where even that generic function will infer T to be a union, though, and then the same problem comes up:但是,在某些情况下,即使是通用的 function 也会推断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

As I said, you'd really like QueueItem to be a union of tuples;正如我所说,您真的希望QueueItem成为元组的联合; you want to consider each key K in keyof Analytics separately, calculate QueueItem<K> for that key, and then unite all the results.您想分别考虑keyof Analytics中的每个键K ,为该键计算QueueItem<K> ,然后合并所有结果。 In other words, you want to distribute QueueItem<K> across unions in K .换句话说,您想在 K 中的联合之间分配QueueItem<K> K One way to do this is with 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] */

Unfortunately, distributive conditional types really only distribute over "naked" type parameters, so the above plays tricks with conditional type inference to turn keyof Analytics into such a naked type parameter.不幸的是,分布式条件类型实际上只分布在“裸”类型参数上,因此上面使用条件类型推断keyof Analytics变成这样的裸类型参数。

I prefer instead to use a mapped type over each key K in keyof Analytics and then index into it with keyof Analytics to get the desired union:我更喜欢K in keyof Analytics使用映射类型,然后使用keyof Analytics对其进行索引以获得所需的联合:

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

Either way works though.无论哪种方式都有效。


You can verify that such a definition of QueueItem results in your desired behavior:您可以验证QueueItem的这种定义是否会产生您想要的行为:

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

Playground link to code Playground 代码链接

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM