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