如何映射和索引到通常在 TypeScript 中传播的元组类型?

How to map & index into tuple types that is generically spread in TypeScript?

TS 4.0 允许传播元组类型和标记的元组类型。

我正在尝试使用这两个功能来创建一种带有上下文的函数或括号模式。

这是我的尝试:

type Resource<T = void> = () => Promise<[
  release: () => Promise<void>,
  resource: T
]>;

async function withF<
  Resources extends Array<Resource<unknown>>,
  Result
>(
  resources: Resources,
  f: (...resources: [...Resources]) => Promise<Result>
): Promise<Result> {
  const releases = [];
  const items = [];
  try {
    for (const resource of resources) {
      const [release, item] = await resource();
      releases.push(release);
      items.push(item);
    }
    return await f(...items);
  } finally {
    releases.reverse();
    for (const release of releases) {
      await release();
    }
  }
}

想法是你可以这样使用它:

let count: number = 0;
await withF(
  [
    async () => {
      ++count;
      return [async () => { --count; }, count];
    }
  ],
  async (c: number) => {
    return c;
  }
);

问题是类型不匹配,因为在我的:

  f: (...resources: [...Resources]) => Promise<Result>

Resources 扩展了 Array<Resource<unknown>>,我想说的是 f 对 [=15= 的每个 return 类型承诺采用第二个元素的传播].

第一个挑战是如何将映射类型映射到 Resourceshttps://devblogs.microsoft.com/typescript/announcing-typescript-3-1/#mappable-tuple-and-array-types.

似乎应该可以

第二步是应用索引选项。哪个也应该在映射类型中工作。但我还是不确定该怎么做。

理想情况下,我们需要某种类型的构造函数来完成它:

  f: (...resources: [...TC<Resources>]) => Promise<Result>

其中 TC 是一个特殊的类型构造函数,它将 Resources 映射到每个 return 类型的第二个元素,并且仍然保留元组长度和顺序。


映射到函数元组的进一步尝试:

type Functions = ((...args: any) => unknown)[];
type FunctionReturns<T extends [...Functions]> = { [K in keyof T]: ReturnType<T[K]> };

const fs: Functions = [() => 1, () => 'abc'];

type FsReturns = FunctionReturns<typeof fs>;

无论出于何种原因,即使映射到元组类型的基本能力有效,这里的 ReturnType 仍然会抱怨,即使我们已经说过 T 扩展了一个函数数组。似乎 ReturnType 在尝试映射到元组类型时似乎不起作用。

可以使用类似于 的方法将 Resources 映射到它们的类型(正如您所发现的),使用 T extends [U] | U[] 的约束将使编译器为 T 推断出 U 的元组,而不是 U.

的数组

一旦到位,我们就会遇到打字稿不确定映射类型的结果是否一定是数组的问题。我们可以通过添加与 unknown[]

的交集来解决这个问题
type ReturnsOfResources<T extends Resource<any>[]> = {
  [P in keyof T] : T[P] extends Resource<infer R> ? R: never
}

async function withF<
  Resources extends [Resource<unknown>] | Array<Resource<unknown>>,
  Result
>(
  resources: Resources,
  f: (...resources: ReturnsOfResources<Resources> & unknown[]) => Promise<Result>
): Promise<Result> {
  const releases = [];
  const items = [];
  try {
    for (const resource of resources) {
      const [release, item] = await resource();
      releases.push(release);
      items.push(item);
    }
    return await f(...items as ReturnsOfResources<Resources>);
  } finally {
    releases.reverse();
    for (const release of releases) {
      await release();
    }
  }
}

Playground Link

如果你想获得一个使用 as const 断言的版本,你将不得不更改代码以处理由 as const 生成的只读元组,同样在创建元组时,你将需要容器元组的断言以及从资源创建函数返回的元组。


type ReturnsOfResources<T extends readonly Resource<any>[]> = {
  -readonly [P in keyof T]: T[P] extends Resource<infer R> ? R : never
}

async function withF<
  Resources extends readonly [Resource<unknown>] | readonly Resource<unknown>[],
  Result
>(
  resources: Resources,
  f: (...resources: ReturnsOfResources<Resources> & unknown[]) => Promise<Result>
): Promise<Result> {
  const releases = [];
  const items = [];
  try {
    for (const resource of resources) {
      const [release, item] = await resource();
      releases.push(release);
      items.push(item);
    }
    return await f(...items as any);
  } finally {
    releases.reverse();
    for (const release of releases) {
      await release();
    }
  }
}

async function x() {
  let count: number = 0;

  const resources = [
    async () => {
      ++count;
      return [async () => { --count; }, count] as const;
    },
    async () => {
      return [async () => { }, 'count']  as const;
    }
  ] as const

  await withF(
    resources,
    async (c, cs) => {
      return c;
    }
  );
}

Playground Link