TypeScript
TypeScript copied to clipboard
Generic inference to unknown after using function arguments
Bug Report
Hello 👋🏻 I'm facing some strange issue with Generics. Until I specify input arguments in function all generics are inferred correctly. But as soon as I mention argument in function the inference is broken.
Thanks in advance for any help 🙏🏻
🔎 Search Terms
- generic is lost after specifying input arguments
- typescript generic unknown after specifying arguments (found this stackoverflow but not sure if it's the same case)
🕗 Version & Regression Information
typescript: 4.2.3 Also tried nightly build but the issue remains
- This is the behavior in every version I tried, and I reviewed the FAQ for entries about Generics_____
⏯ Playground Link
💻 Code
type Connector = <
AccessToken
>(opts: {
getAccessToken(input: {code: string}): Promise<AccessToken>;
validateAuth(input: {fields: AccessToken}): Promise<{name: string}>;
}) => any;
const connector: Connector = (inp) => undefined
connector({
getAccessToken: async () => ({token: 'token'}),
validateAuth: async ({fields}) => { //fields: {token: string}
// ^?
return {name: 'qwe'}
}
})
connector({
getAccessToken: async (inp) => ({token: 'token'}), // mention input argument breaks inference
validateAuth: async ({fields}) => { // fields: unknown
// ^?
return {name: 'qwe'}
}
})
🙁 Actual behavior
fields inside validateAuth infer to unknown after using function argument in getAccessToken. Until I use arguments everything works OK
🙂 Expected behavior
Usage of function arguments inside getAccessToken should not affect Generic inference since they are statically type
type Connector = <
AccessToken
>(optsA: {
getAccessToken(input: {code: string}): Promise<AccessToken>;
}, optsB: {
validateAuth(input: {fields: AccessToken}): Promise<{name: string}>;
}) => any;
const connector: Connector = (inp) => undefined
connector({
getAccessToken: async () => ({token: 'token'}),
}, {
validateAuth: async ({fields}) => { //fields: {token: string}
// ^?
return {name: 'qwe'}
}
})
connector({
getAccessToken: async (inp) => ({token: 'token'}), // mention input argument breaks inference
}, {
validateAuth: async ({fields}) => { // fields: {token: string}
// ^?
return {name: 'qwe'}
}
})
This issue might be related: 38264.
Our inference algorithm is based on doing a fixed number of passes to collect inference candidates (see also #30134 which proposes that we use an unbounded number of passes). In this case, the parameterless function is non-context-sensitive, meaning that its return type can be known ahead-of-time to not depend on the ultimate result of inference, so we're able to use its return type to collect an inference candidate.
The parameterful version is context-sensitive, because it has an unannotated parameter. From the outside we can see that this ultimately won't matter, but the inference process is not far enough along at this point to use that information, and thus delays collecting the return type as an inference candidate until later on at which point AccessCode has already been fixed to unknown due to a lack of covariant candidates.
The workaround in this case is to write (inp: { code: string }) =>.
wow, thanks for the detailed answer and the workaround
Thanks for the great explanation @RyanCavanaugh, very helpful! Ran into this exact issue today (playground link), and it was especially insidious because it silently introduced anys into the codebase without causing any compiler errors. Leaving a slightly simplified example here just in case it's useful for discussions/example use cases that could be improved. It'd be much less of a priority for me if it just introduced unknowns instead of anys, because that would be caught easily in other places at compile time:
type InputData = { someNum: number; }
type Options<T> = {
selector: (data: InputData) => T;
equalityFn?: (a: T, b: T) => boolean;
}
function useSelector<T>(options: Options<T>): T {
return options.selector({someNum: 20});
}
// GOOD: works when there's an implict type for `data` and no `equalityFn`,
// `foo` is correctly of type `number`
const { foo } = useSelector({
selector: (data) => ({ foo: data.someNum }),
});
// GOOD: works when there's an expicit type for `data` and an `equalityFn`
// `foo2` is correctly of type `number`
const { foo2 } = useSelector({
selector: (data: InputData) => ({ foo2: data.someNum }),
equalityFn: (a,b) => a === b,
});
// BAD: fails silently when there's an implicit type for `data` and an `equalityFn`
// foo3 is `any`
const { foo3 } = useSelector({
selector: (data) => ({ foo3: data.someNum }),
equalityFn: (a,b) => a === b,
});
@jkillian that's a bug caused by the binding pattern (aka destructuring). I think it's reported already but can't find it at the moment - can you log a new issue?
@RyanCavanaugh, thanks, agreed after looking at it again that the any vs. unknown part of my above post is a different issue than this ticket about function arguments and inference. I've made the new issue for that here: https://github.com/microsoft/TypeScript/issues/45074. Please feel free to edit anything as necessary if I used wrong terminology anywhere (as is highly likely 😆).
Closed #46977 (duplicate) for this one. We're trying to do get around this in our React Query library and it's definitely been weirding us out 😂 . I think it's worth dropping in a quick snippet of our use-case for posterity:
function useQuery<TQueryKey, TData>(_options: {
queryKey: TQueryKey;
queryFn: (context?: { queryKey: TQueryKey }) => TData;
onSuccess: (data: TData) => void;
}) {}
const queryKey = ["test", 1, 2, { 3: true }]
// no context no cry
useQuery({
queryKey,
queryFn: (): number => 1,
onSuccess: (data) => data.toFixed(),
});
// as soon as I use ctx, data is no longer of type number for onSuccess
useQuery({
queryKey,
queryFn: (ctx): number => 1,
// Why is `data` of type `any`?
onSuccess: (data) => data.toFixed(),
});
For now, we've dropped the proposal to adopt this syntax as the primary syntax in our API, but we would really love to make it happen in the future. Where should we go from here to help that happen? cc @tkdodo
I believe this is related to #47599 and is now fixed by #48538?
@RyanCavanaugh it seems that this got fixed by intra-inference improvements in 4.7, all reported playgrounds work:
The issue can be closed.