Issue
I have a case where contextual inference fails for a previously declared generic function. I don't understand why it fails but providing a function without generic arguments is contextually inferred successfully.
I have the restriction that the argument projector
must come last. This is to type the createSelector
function of NgRx: https://github.com/ngrx/platform/issues/3268.
const state: { counter: number } = { counter: 0 };
const getSame = <T>(same: T): T => same;
const returnState = () => state;
declare function createSelector<Slices extends unknown[], U>(...args: [...getSlices: { [i in keyof Slices]: () => Slices[i] }, projector: (...args: Slices) => U]): (...args: unknown[]) => U;
const variadicSlicesWithContextualInference = createSelector(returnState, d => d);
const test1 = variadicSlicesWithContextualInference(state).counter;
const variadicSlicesWithContextualInferenceOfGeneric = createSelector(returnState, <T>(d: T): T => d);
// 👇 inference fails, variadicSlicesWithContextualInferenceOfGeneric(state) is unknown
const test2 = variadicSlicesWithContextualInferenceOfGeneric(state).counter;
const variadicSlicesWithContextualInferenceOfDeclaredFn = createSelector(returnState, getSame);
// 👇 inference fails, variadicSlicesWithContextualInferenceOfDeclaredFn(state) is unknown
const test3 = variadicSlicesWithContextualInferenceOfDeclaredFn(state).counter;
// But if I declare a function specific for one "slice", then inference works properly
declare function createSelectorOneSlice<T, U>(...args: [getSlice: (...args: unknown[]) => T, projector: (...args: [T]) => U]): (...args: unknown[]) => U;
const oneSlice = createSelectorOneSlice(returnState, getSame);
const test4 = oneSlice(state).counter;
Solution
My suspicion here is that this is not currently possible in TypeScript. The inference algorithm is not "full unification" as requested/discussed in microsoft/TypeScript#30134; instead it is more heuristic in nature and, while it often does what people want, has limitations.
You're using leading elements in tuple types here, which seems to be complicating things. If you were able to re-order the parameters so that the variadic part came at the end (like a well-behaved rest parameter should):
declare function createSelector<S extends unknown[], U>(...args: [
projector: (...args: S) => U,
...getSlices: { [I in keyof S]: () => S[I] }
]): (...args: unknown[]) => U;
or equivalently
declare function createSelector<S extends unknown[], U>(
projector: (...args: S) => U,
...getSlices: { [I in keyof S]: () => S[I] }
): (...args: unknown[]) => U;
then things would probably work better:
const variadicSlicesWithContextualInferenceOfDeclaredFn = createSelector(getSame, returnState);
variadicSlicesWithContextualInferenceOfDeclaredFn(state).counter; // okay
Unfortunately you can't do this.
Since your variadic version works just fine for non-generic getSlices
elements, I think your best bet would be a hybrid approach where you declare a series of overloaded call signatures for some non-variadic getSlices
sizes, to capture the most likely use cases.
When developers call createSelector()
, how many parameters do they typically pass? Probably just a few. So you can deal with those few to get the desired inference, and then fall back to the variadic version:
// 0
declare function createSelector<U>(
projector: () => U): (...args: unknown[]) => U;
// 1
declare function createSelector<A, U>(
getSliceA: () => A,
projector: (a: A) => U): (...args: unknown[]) => U;
// 2
declare function createSelector<A, B, U>(
getSliceA: () => A,
getSliceB: () => B,
projector: (a: A, b: B) => U): (...args: unknown[]) => U;
// 3
declare function createSelector<A, B, C, U>(
getSliceA: () => A,
getSliceB: () => B,
getSliceC: () => C,
projector: (a: A, b: B, c: C) => U): (...args: unknown[]) => U;
// n
declare function createSelector<S extends unknown[], U>(...args: [
...getSlices: { [I in keyof S]: () => S[I] },
projector: (...args: S) => U]
): (...args: unknown[]) => U;
and this also works:
const variadicSlicesWithContextualInferenceOfDeclaredFn = createSelector(returnState, getSame);
variadicSlicesWithContextualInferenceOfDeclaredFn(state).counter; // okay
So, hooray!
Overloads do have drawbacks; for example, when you're not directly calling the overloaded function, but instead trying to have the compiler infer something about the type of an overloaded function, it tends to just ignore all but the first or the last call signature, which can be confusing and surprising. (See the documentation for inferring in conditional types where it talks about overloads). And in this case the overloads will be redundant and more annoying to maintain. But if you care about callers having their generic getSlice
elements inferred properly, overloads are the best way I can think of to get that to happen.
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.