如何从 TypeScript 中的可变通用元组创建新元组?

How do I create a new tuple from a variadic generic tuple in TypeScript?

给定:

class Foo {}
class Bar {}

interface QueryResult<T> {
  data: T;
}

function creatQueryResult<T>(data: T): QueryResult<T> {
  return {data};
}

function tuple<T extends any[]>(...args: T): T {
  return args;
}

我想使用接受 QueryResult[]QueryResult 元组的推断类型和一个选择 data 并调用回调的工厂回调来创建和键入一个函数,如:

function createCompoundResult<T>(
  queryResults: T,
  callback: (queryResultDatas: ???<T>) => any
) {
  const datas = queryResults.map((queryResult) => queryResult.data);

  return callback(datas);
}

注意上面代码中的???

用法:

const fooQueryResult = creatQueryResult(new Foo());
const barQueryResult = creatQueryResult(new Bar());

// Maybe using tuples are wrong?
const queryResults = tuple(fooQueryResult, barQueryResult);

createCompoundResult(
  queryResults,
  (datas) => {
    const [foo, bar] = datas;
    // foo should be inferred as Foo here and bar as Bar
  }
);

也许元组是错误的方法?你会怎么解决?

我是一个 TypeScript 新手,我很难理解像 keyofextends keyof{ [K in keyof T]: { a: T[K] } } 这样的东西,所以如果你的解决方案包括这样的神秘魔法,请像我5岁一样给我解释一下。

我对 createCompoundResult() 的建议是:

function createCompoundResult<T extends any[]>(
  queryResults: readonly [...{ [I in keyof T]: QueryResult<T[I]> }],
  callback: (queryResultDatas: readonly [...T]) => any
) {
  const datas = queryResults.map((queryResult) => queryResult.data) as T;
  return callback(datas);
}

函数是 generic in T, corresponding to the tuple of arguments to callback. In order to describe the type of queryResults in terms of the array/tuple type T, we want to map it to another array/tuple type,其中对于 T 的每个数字索引 I,元素类型 T[I] 映射到 QueryResult<T[I]>。所以如果 T[string, number],那么我们希望 queryResults 的类型是 [QueryResult<string>, QueryResult<number>].

您可以通过 mapped type. It looks like { [I in keyof T]: QueryResult<T[I]> }. For array-like generic types T, mapped types like [I in keyof T] only iterate over the numeric-like keys I (and skip all the other array keys like "push" and "length"). So you can imagine { [I in keyof T]: QueryResult<T[I]> } acting on a T of [string, boolean] operating on I being "0" and then "1", and T["0"] is string and T["1"] is boolean, so you get {0: QueryResult<string>, 1: QueryResults<boolean>}, which is magically interpreted as a new tuple type [QueryResult<string>, QueryResult<boolean>].

这是主要的解释,尽管还有一些突出的事情要提。


首先是编译器不知道the array map() method will turn a tuple into a tuple, and it definitely doesn't know that queryResult => queryResult.data will turn a tuple of type { [I in keyof T]: QueryResult<T[I]> } into a tuple of type T. (.) It sees the output type of your queryResults.map(...) line as T[number][], meaning: some array of the element types of T. It has lost length and order information. So we have to use a type assertion告诉编译器queryResults.map(...)的输出是T类型,这样datas就可以传递给 callback.

接下来,我在 readonly [...AAA] 中包装了数组类型 AAA 的几个地方。这使用 variadic tuple type 语法向编译器提示我们希望它推断元组类型而不是数组类型。如果你不使用它,那么 [fooQueryResult, barQueryResult] 之类的东西往往会被推断为数组类型 Array<QueryResult<Foo> | QueryResult<Bar>> 而不是所需的元组类型 [QueryResult<Foo>, QueryResult<Bar>]。使用此语法使我们无需使用 tuple() 辅助函数,至少如果您直接传递数组文字。


无论如何,让我们确保它有效:

class Foo { x = 1 }
class Bar { y = 2 }

createCompoundResult(
  [fooQueryResult, barQueryResult],
  (datas) => {
    const [foo, bar] = datas;
    foo.x
    bar.y
  }
);

看起来不错。我给了 FooBar 一些结构(即使是示例代码也总是 recommended 这样做)并且果然,编译器理解 datas 是一个元组,它的第一个元素是 Foo,其第二个元素是 Bar.

Playground link to code