Mapped type not capable of inferring exact tuple (argument) element when generics explicitly set? Any workaround?

77 views Asked by At

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:

  1. Argument is a SELECTOR_NAME, in which case we have the information in the SELECTORS_COLLECTION
  2. 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.

0

There are 0 answers