Let's discuss the implementation of buildSelector()
:
function buildSelector<T extends any[], U>(...args: [
...selectors: { [I in keyof T]: (state: State) => T[I] },
callback: (...args: T) => U
]): (state: State) => U {
const selectors = args.slice(0, -1) as ((state: State) => any)[];
const callback = args.at(-1) as (...args: any) => U;
return (state) => callback(...selectors.map(f => f(state)))
}
The function signature might seem convoluted due to the need to accept multiple selectors before a single callback function. This setup resembles using a rest parameter with an additional parameter afterward. However, JavaScript restricts placing the rest parameter at the end of the parameter list. If we envisioned a hypothetical scenario where JavaScript allowed leading rest elements, the function structure could be represented like so:
// This is neither valid TypeScript nor valid JavaScript, do not use:
function buildSelector<T extends any[], U>(
...selectors: { [I in keyof T]: (state: State) => T[I] },
callback: (...args: T) => U
): (state: State) => U {
return (state) => callback(...selectors.map(f => f(state)))
}
In the theoretical case above, the function would work straightforwardly, accepting state and invoking the callback on a map
operation on selectors. Nonetheless, such logic isn't directly transferrable to real-world scenarios owing to complexities addressed by type assertions.
Transitioning back to reality:
function buildSelector<T extends any[], U>(...args: [
...selectors: { [I in keyof T]: (state: State) => T[I] },
callback: (...args: T) => U
]): (state: State) => U {
const selectors = args.slice(0, -1) as ((state: State) => any)[];
const callback = args.at(-1) as (...args: any) => U;
return (state) => callback(...selectors.map(f => f(state)))
}
Due to variable-length arguments with the variable part at the start, the function necessitates a lone args
rest parameter in the beginning. Despite JavaScript lacking support for a lead rest parameter, TypeScript features a leading rest tuple element, accommodating this design. Consequently, args
becomes a variadic tuple containing selectors followed by a final callback element, mirroring previous specifications.
To disassemble args
within the function body, conventional destructuring assignment fails due to Javascript limitations. Thus, methods like slice()
and at()
come into play. The implementation flexibility remains open to personal preference.
Type assertions and usage of the any
type form a crucial part of ensuring loose enough types for selectors
and callback
, preventing compiler errors during
return (state) => callback(...selectors.map(f => f(state)))
. These operations compensate for the intricacies involved when manipulating tuple arrays and represent compromises given TypeScript constraints.
Evaluating the practical application:
const selector = buildSelector(
selectName, selectAge, (name, age) => `${name} - ${age}`
);
const x = selector(state);
// const x: string
console.log(x) // "John Doe - 38"
Successful inference by the compiler contextualizes name
and age
parameters within the final callback as string
and number
correspondingly. Likewise, understanding selector
as (state: State)=>string
based on the callback return type showcases operational coherence.
Note that actual implementation gets validated empirically amidst the compilation intricacies faced. The iterative testing phase validates overarching functionality.