I am having trouble understanding how/why the mapped type is unable to handle explicitly typed arguments but is working perfectly fine with default types (and is able to infer exact args)? More importantly, is there a way to keep the behavior that we see for default types, whilst constraining the arguments? Essentially is there a way to NOT lose the "tuple" whilst constraining it?
This TSPlayground link contains all the types / code presented in the question.
type Selector<S = any, R = unknown> = (state: S) => R
type SELCTOR_NAME = keyof SELECTOR_COLLECTION
type SELECTOR_COLLECTION = {
foo: string,
bar: number
}
type Dependency<S = any> = SELCTOR_NAME | Selector<S>
Assuming all the selectors in our codebase have an entry in this collection, we could then create a function which makes a cached selector. This function would take in dependencies (which are either other Selectors or SELECTOR_NAME-ish strings) and return a memoized selector.
For the sake of this question: The return type of this function has been changed in order to illustrate the issue - function would, normally, return a cached selector. For that to happen however we need to map the argument types and "extract" the information regarding the return types of those arguments.
declare function cachedSelector<G = any, T extends Dependency[] = Array<Dependency<G>>>(
...args: T
): {
[I in keyof T]: T[I] extends SELCTOR_NAME
? () => SELECTOR_COLLECTION[T[I]]
: T[I]
}
Since the arguments of the function are constrained to the T extends Dependency[] type we have two cases:
- Argument is a
SELECTOR_NAME, in which case we have the information in theSELECTORS_COLLECTION - Argument is a
Selector, in which case we have the return type present, as this is a function.
// result = [() => string, () => number, () => boolean]
const result = cachedSelector("foo", "bar", () => true)
As we can see, this works just fine, however if we explicitly define the type argument G (even if we define it as it's default value any), the whole thing falls appart:
// when explicitly defined, result = Dependency<any>[]
const result = cachedSelector<any>("foo", "bar", () => true)
// EDIT: same behavious is observed when both arguments are specified:
const resultBoth = cachedSelector<any, Dependency<any>[]>("foo", "bar", () => true)
To my understanding the mapping happens, but the conditional type is always falsy. It seems as if, when G is defined explicitly all the arguments simply default to being Dependency<G> instead of being the actual arguments that were passed to the function, so the conditional type fails.