Creating a solution where the compiler automatically infers types as desired without additional effort when calling the function seems difficult.
There is an identified design limitation in TypeScript, discussed in microsoft/TypeScript#38872, where the compiler struggles to simultaneously infer generic type parameters and contextual types for callback parameters that depend on each other. When making a call like:
return makeNexters({}, [
async (data) => { return { ...data, a: 3 }; },
async (data) => { return { ...data, b: 'hi' }; },
async (data) => {},
])
The request to the compiler involves using {}
for inferring some generic type parameter, which then influences the type of the first data
callback parameter, subsequently affecting the inference of another generic type parameter linked to the next data
callback parameter, and so forth. The compiler can only handle a limited number of type inference phases before giving up after potentially the initial one or two inferences.
Ideally, I propose expressing the type of makeNexters()
somewhat like this:
// Type definition for makeNexters
type Idx<T, K> = K extends keyof T ? T[K] : never
declare function makeNexters<T, R extends readonly any[]>(
init: T, next: readonly [...{ [K in keyof R]: (data: Idx<[T, ...R], K>) => Promise<R[K]> }]
): void;
This type specification indicates that the init
parameter has a generic type T
, while the next
parameter functions with tuple type mapping R
. Each element within next
should be a function accepting a data
parameter from the "previous" element in R
(except for the first one accepting it from
T</code), and returning a <code>Promise
for the "current" element in
R
.
(The void
return value concern isn't pivotal here at present due to focusing on type inference)
This method does function effectively but not exactly in the preferred way of type inference. It's possible to prioritize contextual type inference for callback parameters over generic type inference:
// Prioritizing contextual type inference
makeNexters<{}, [{ a: number; }, { b: string; a: number; }, void]>()
Or vice versa, emphasizing generic type inference at the expense of contextual type inference:
// Emphasizing generic type inference
makeNexters<{}, [{ a: number }, { b: string, a: number }, void]>({})
An attempt to achieve both types of inference concurrently results in ineffective any
inferences throughout:
// Ineffective mixed inference
makeNexters({}, [
async (data) => { return { ...data, a: 3 }; },
async (data) => { return { ...data, b: 'hi' }; },
async (data) => { }
]);
/* function makeNexters<{}, [any, any, void]>*/
Situations requiring manual input for typing like {b: string, a: number}
diminish the purpose behind this chained function concept.
Prior to conceding completely, consider altering the strategy towards a nested architecture where each link in the chain originates from a separate function call. This aligns with the builder pattern, constructing the "nexter" incrementally instead of all at once via a single array representation:
// Alternative approach using builder pattern
const p = makeNexterChain({})
.and(async data => ({ ...data, a: 3 }))
.and(async data => ({ ...data, b: "hi" }))
.done
The resulting p
object type would match the original nested version:
// Output type matching nested version
/* const p: {
data: {};
next: () => Promise<{
data: {
a: number;
};
next: () => Promise<{
data: {
b: string;
a: number;
};
next: () => Promise<void>;
}>;
}>;
} */
The implementation details of makeNexterChain()
fall beyond the scope here, focused more on typings rather than runtime behavior. Construct makeNexterChain()
considering appropriate utilization of promise's then()
method.
In essence, makeNexterChain()
starts with an initial element of type T
, producing a NexterChain<[T]>
. Each NexterChain<R>
(with R
being a tuple type) offers an and()
method to append a new type to the end of R
, alongside a done()
method returning a Nexter<R>
. A Nexter<R>
includes a data
property reflecting the first element of R
, along with a next()
method sans arguments, generating a Promise
for a fresh Nexter<T>
identical to
R</code minus the initial element. Eventually leading to <code>void
at termination.
This demonstrated method exhibits exceptional type inference clarity at each step, alleviating the need for explicit generic parameter definition or callback parameter annotation. Explore such solutions utilizing TypeScript's robust type inference functionality seamlessly.
Playground link to code