Repository navigation
infer function generic signature from actual function's return type #49618
Description
Activity
It's not that TS doesn't infer the generics, in general, but just that it's inferring it as
unknownin this specific case.test(() => ({ a: 1 })); // T is inferred as "{ a: number }" as you might expect
It looks like the issue here is the unannotated
prev, since that'sunknown,Tends up asunknown.In this very simple case (where
previs unused), it's fairly easy to see what the right behavior would be, but I suspect that evaluating the return type of a function and using that to infer the type of its arguments (which usually affect the return type) is not a simple thing to do.RyanCavanaugh commented
on Jun 21, 2022 MemberMore actionsTwo cases to consider here
The first case is where the function expression uses the parameter but the return type of the function provably doesn't depend on the parameter type. In practice, it's very rare for these kinds of function expressions to exist - they're by definition impure, and given control flow effects it's very difficult to even construct such a function.
The second case is where the function expression doesn't use the parameter, as is the case here. They can be removed WLOG, and after doing so the parameter is inferred successfully as expected.
So on balance there's not really much gain to be had here - the inference is still sound, and detecting if a used parameter has no effect on the return type of a function is a very difficult calculation to always get right
- addedSuggestionAn idea for TypeScriptAn idea for TypeScriptToo ComplexAn issue which adding support for may be too complex for the value it addsAn issue which adding support for may be too complex for the value it adds
on Jun 21, 2022 WLOG
I had to google this acronym. First time I've ever encountered it.
To me it feels like TS should be treating
(prev) => 42as equivalent to() => 42for the purpose of generic inference--that is to say,previs not a valid inference site forTsince it has no type annotation and can only later be typed through contextual typing--which requiresTto be known first. If it weren't for the generic,(prev) => 42would be an implicitanyerror. Effectively, the presence of the generic creates this weird pseudo-circular situation whereTis indirectly inferred from its own constraint via contextual typing, which feels... wrong.RyanCavanaugh commented
on Jun 21, 2022 MemberMore actionsYeah, I'll reopen #47599 since it's not fully covered
RyanCavanaugh commented
on Jun 21, 2022 MemberMore actionsThat said, I'm not really sure what the endgoal is. Any "improvement" we make here is just going to sow a bunch of "TypeScript is inconsistent, therefore has bug" reports because people will wonder why
function test<T>(fn: (prev: T) => T) { } test((prev) => { return 0; });
works but not
function test<T>(fn: (prev: T) => T) { } test((prev) => { return prev ? 0 : 1; });
The current behavior is at least very explainable and easy to reason about; moving the needle into the grey zone just raises more questions than it does solve problems.
Yeah, I understand. What bothers me is mostly theoretical - the fact that the current behavior basically amounts to:
- What is
T? - Ooh,
previs aT, we can inferTfrom the callback the caller passed in... - ...except
prevhas no type annotation! - Contextual typing says
previs aT, but we don't know whatTis yet... - Screw it, we'll say
prev: unknown(i.e. the constraint ofT). - Therefore,
Tisunknown.
It feels like a bug, even if the "correct" behavior isn't really qualitatively "better" in practice.
- What is
To be clear, my problem isn’t the
prev: unknownpart (that part makes sense), but the fact thatTultimately has its own constraint as its inference site. I guess there’s no mechanism to fix the latter without compromising the former, though.RyanCavanaugh commented
on Jun 21, 2022 MemberMore actionsI believe what actually happens is that we collect candidates for
T, find out there are none, so default it to its constraint, which isunknown, and then process the call as if you had writtentest<unknown>(.It's weird since if you had written
test<unknown>(, that definitely shouldn't be an implicitanyerror. Maybe we need to make a markerunknownto use in zero-candidate inference that isn't allowed to contextually type a parameter - worth experimenting with, probably.Reacted by Bruce PascoeI think a big part of what makes this case so tricky is that
Tis found in both covariant and contravariant positions. If you infer only from the covariant (return) position, you’re likely to end up with a type that’s too narrow, as you show in the examples above. I think the only change that would make sense is for this to become an error, a la implicit-any, as in the absence of a type annotation on the callback parameter, the contravariant position can’t be inferred from without always "inferring" the constraint. I acknowledge turning this into an error is a potentially disruptive breaking change, though.RyanCavanaugh commented
on Jun 21, 2022 MemberMore actionsWesley Wigham (@weswigham) pointed out this example
type Box<T> = { contents: T }; function test<T>(fn: (prev: Box<T>) => T) { } test((prev) => ({ a: 1 }));
It's not clear how we'd turn this into an error -- somehow it's (speculatively) a "
Box<implicit any>" which isn't really a thing - implicit any arises when a binding should have a contextual type but doesn't, but that's not what's happening here. And zero-candidate inference is not a manifest error either:function test<T>(x?: T): T; test();
So there seems to be some difficulty in establishing what rule exactly would turn the OP example into an error without either breaking something that shouldn't be broken (benign zero-candidate inference), failing to break something equally suspect (
Box<), or both.blaumeise20 commented
on Jun 22, 2022 ContributorMore actionsDoes TypeScript have a Hindley-Milner type system? Because I'm pretty sure it would work with that kind of type inference.
TypeScript would see that the return type and first parameter have to be the same, so it could check for return type and see "oh it's an object literal with type
{ a: number }". And because it knows thatprevmust be of the same type, it can say thatprevis of type{ a: number }too.It seems like it doesn't work like that now, right?
No, TS's type system is not H-M. Type inference is done locally. See #30134.
Your example of inferring
prevbased on the return type was discussed above:function test<T>(fn: (prev: T) => T) { } test((prev) => { return prev ? 0 : 1; });
would infer
T = 0 | 1under your proposed behavior, which isn't ideal (numberwould be more useful). In general this behavior would tend to infer types that are too narrow. It's a tricky case because you generally want to infer wider types for a parameter but narrower types for a return type, but here they're required to be the same type.Interesting points above.
I can describe a real-world use case. Suppose we have a
memofunction that is reactive, and re-runs an expression any time dependencies used inside of the function body change. Thememotool can be called in two ways. The first way:const fname = reactiveVar('John') const lname = reactiveVar('Doe') const fullname = memo((prev) => { return fname.get() + ' ' + lname.get() }) fullname() // string, combination of fname + lname
In this first case,
prevarguments can only ever bestring | undefined, where on the very first run of the reactive expression the initialprevvalue isundefined. It can never be anything other thanstring | undefined. Note though that the return value is neverstring | undefinedbut juststring.Any time that
fname.set(string)orlname.set(string)are called, it causes the function passed tomemoto re-execute to evaluate the newfullnameand trigger other reactive expressions elsewhere that depend onfullname.The second way to use the
memoAPI is like this:const fname = reactiveVar('John') const lname = reactiveVar('Doe') const fullname = memo((prev) => { return fname.get() + ' ' + lname.get() }, "Godzilla Kong") // <--------------------------- HERE, new movie idea fullname() // string, combination of fname + lname
In this case,
prevarguments will only ever bestrings. They will never be anything else, because the initial value use forprevwill be"Godzilla Kong", and the return value is always astring.This works totally fine in plain JavaScript, but the main issue is that in TypeScript, it currently requires too much superfluous type annotation.
Reacted by Bruce PascoeRyanCavanaugh commented
on Aug 26, 2022 MemberMore actionsJoe Pea (@trusktr) did you mean to use
previn these examples? Again we have the problem of "you can just omit the parameter".I have a real-world use case related to a PR in nanostores.
I'm adding a
cxfunction into thecomputedfunction. Thecxfunction takes a callback & causes the computed to auto-listen to dependencies by adding itself to a global stack while the callback executes. I'd like to have the return type of thecxfunction be the return type of the callback.Here is a contrived example to keep it simple. The real use case involves async, so I'll demonstrate with a
sleep.let firstName = atom('') let lastName = atom('') let $userId = atom(0) let fullNameUserId = computed(async cx => { await sleep(100) let userId = cx(() => $userId()) // Should infer as a number but infers as any return cx(() => `${firstName()}-${lastName()}-${userId}`) // Should infer as a string but infers as any }) function sleep(ms: number) { return new Promise(res => { setTimeout(() => res(null), ms) }) } declare function computed<Value extends any, Cx extends ComputedCx<any>>(cx: Cx): ReadableAtom<Value> type ComputedCx<R> = (cb: () => R) => R declare function atom<Value>(initialValue: Value): WritableAtom<Value = any> interface ReadableAtom<Value = any> { // ... } interface WritableAtom<Value = any> extends ReadableAtom<Value> { // ... }
typescript-bot commented
on Jun 21, 2023 ContributorMore actionsThis issue has been marked as "Too Complex" and has seen no recent activity. It has been automatically closed for house-keeping purposes.
is there a workaround?
is there some way to tell typescript to infer the type from the returned value offnby writing it differently?function test<T>(fn: (prev: T | undefined) => T) { }
Suggestion
🔍 Search Terms
Maybe an issue exists, but I wasn't sure what to search for. "Infer generic function type from return value" didn't really help.
✅ Viability Checklist
My suggestion meets these guidelines:
⭐ Suggestion
Tshould be inferred as the type of the returned object,{a: number}📃 Motivating Example
https://www.typescriptlang.org/play?#code/GYVwdgxgLglg9mABFApgZygHgCoD4AUwYAXIvgA4BOKAbqdgJSIC8uijiA3ogL4BQqDPgrUaTVmW4BDUgEZeDBgG4+QA
💻 Use Cases
make life easier