0

TS 4.0 allows spreading tuple types and labelled tuple types.

I'm attempting to use both features to create a sort of with-context function or bracketing pattern.

Here is my attempt:

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();
    }
  }
}

The idea is that you can use it like this:

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

The problem is that the types don't match because in my:

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

The Resources extends Array<Resource<unknown>>, and I want to say that f takes a spread of the second element for each return type promise of Resources.

First challenge is how to do a mapping type into Resources. It seems it should be possible with https://devblogs.microsoft.com/typescript/announcing-typescript-3-1/#mappable-tuple-and-array-types.

The second step is to apply the indexing option. Which should work in the mapping type as well. But again I'm not sure how to do this.

Ideally we want some sort of type constructor that does it:

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

Where TC is a special type constructor that maps Resources to the 2nd element of each return type and still preserves the tuple length & order.


Further attempts for mapping into a tuple of functions:

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>;

For whatever reason, even though basic ability to map into tuple types work, the ReturnType here still complains even though we've said that T extends an array of functions. It seems that ReturnType doesn't seem to work when attempting to map into tuple types.

CMCDragonkai
  • 6,222
  • 12
  • 56
  • 98

1 Answers1

1

The mapping of Resources to their types (as you also found) can be done using something similar to this answer, with the addendum that using a constraint of T extends [U] | U[] will make the compiler infer a tuple of U for T instead of an array of U.

Once that is in place we have the issue that typescript is unsure that the result of the mapped type will necessarily be an array. We can get around this by adding an intersection with 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

If you want to get a version working with as const assertions you will have to change the code to deal with the readonly tuples generated by as const, also when creating the tuple, you will need assertions on the container tuple as well as the tuples returned from resource creating function.


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

Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • One of the problems is that the resource acquisition returns a promise. So we actually need to use `Awaited` to extract out the internal type. At this point in time, the playground link works but doesn't work in my tsc. – CMCDragonkai Jan 17 '22 at 08:14
  • Actually I realised I don't need `Awaited` at all. So it's fine. I just need to add types to `const releases` and `const resources`. – CMCDragonkai Jan 17 '22 at 08:28
  • One thing I noticed is the lack of `readonly` applied here compared to your previous answer https://stackoverflow.com/a/60713409/125734. Wondering what the impact of this is. – CMCDragonkai Jan 17 '22 at 08:29
  • Also if I create `resources` in a separate definition, even if I add `as const`, I get type errors. Like `await withF(resources, async (c, cs) => { return c; })`, the `resources` doesn't type check. – CMCDragonkai Jan 17 '22 at 08:40
  • @CMCDragonkai Updated the answer to work with readonly tuples too – Titian Cernicova-Dragomir Jan 17 '22 at 08:55
  • That requires `as const` to be applied in the internal tuples as well which can be quite verbose. Another solution was to give it an explicit type `[Resource, Resource]` which ended up working. – CMCDragonkai Jan 17 '22 at 09:03
  • @CMCDragonkai Yeah. Explicit annotation is also an option – Titian Cernicova-Dragomir Jan 17 '22 at 10:07
  • Cool this works. But I noticed another little issue. The `f` callback must have either no arguments, or the number of arguments must match the exact number of resources. E.g. there's no way to only have a callback that takes 1 resource parameter if there 2 resource acquisitions. TS complains that the length property doesn't match. – CMCDragonkai Jan 18 '22 at 03:56
  • I think I found a solution, instead of spreading the resources into `f`, just pass the array/tuple directly in. This seems to simplify the types too. – CMCDragonkai Jan 18 '22 at 04:10