- Notifications
You must be signed in to change notification settings - Fork13.2k
Higher order function type inference#30215
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Conversation
jack-williams commentedMar 4, 2019 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Is this the correct behaviour? functionap<A>(fn:(x:A)=>A,x:A):()=>A{return()=>fn(x);}declarefunctionid<A>(x:A):Aconstw=ap(id,10);// Argument of type '10' is not assignable to parameter of type 'A'. [2345] I think the logic that floats type parameters out when the return value is a generic function might be too eager. In existing cases where I haven't looked into this much, so apologies in advance if I've got something wrong. |
ahejlsberg commentedMar 4, 2019
@jack-williams The core issue here is the left to right processing of the arguments. Previously we'd get it wrong as well (in that we'd infer |
jack-williams commentedMar 5, 2019 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
@ahejlsberg Ah yes, that makes sense. I agree that it was previously 'wrong', but I disagree that it was a loss of type safety (in this example). The call is perfectly safe, it's just imprecise. Previously this code was fine: constx=ap(id,10)();if(typeofx==="number"&&x===10){constten:10=x;} but it will now raise an error. Could this be a significant issue for existing code? I'll just add the disclaimer that this is really cool stuff and I'm not trying to be pedantic or needlessly pick holes. I'd like to be able to contribute more but I really haven't got my head around the main inference machinery, so all I can really do is try out some examples. |
RyanCavanaugh commentedMar 5, 2019
The safe/imprecise distinction is sort of lossy given certain operations that are considered well-defined in generics but illegal in concrete code, e.g. functionap<A>(fn:(x:A)=>A,x:A):(a:A)=>boolean{returny=>y===x;}declarefunctionid<A>(x:A):Aletw=ap(id,10)("s"); or by advantage of covariance when you didn't intend to functionap<A>(fn:(x:A)=>A,x:A[]):(a:A)=>void{returna=>x.push(a);}functionid<A>(x:A):A{returnx;}constarr:number[]=[10];ap(id,arr)("s");// prints a string from a number arrayconsole.log(arr[1]); |
jack-williams commentedMar 5, 2019
I think being lossy in isolation is ok; it's when you try and synthesise information that you get into trouble. The first example is IMO completely fine. The conjunction of union types, and equality over generics makes comparisons like that possible. functioneq<A>(x:A,y:A):boolean{returnx===y;}eq<number|string>(10,"s");eq(10,"s");// error It's just the behaviour of type inference that rejects the second application because it's probably not what the user intended. I accept that my definition of 'fine' here comes from a very narrow point-of-view, and that being 'fine' and unintentional is generally unhelpful. I agree that the second example is very much borderline, though stuff like that is going to happen with covariance because you end up synthesising information. I'm not trying to defend the existing behaviour; I'm all in favour of being more explicit. My only concern would stem from the fact that the new behaviour is somewhat unforgiving in that it flags the error immediately at the callsite. Users that previously handled the On a slightly different note I wonder if the new inference behaviour could come with some improved error messages. There is along way to go until TypeScript reaches the cryptic level of Haskell, but with generics always comes confusion. In particular, I wonder in the case of: constw=ap(id,10); The error message is |
KiaraGrouwstra commentedMar 5, 2019
zpdDG4gta8XKpMCd commentedMar 5, 2019
best day ever! thank you so much@ahejlsberg and the team you made my day! i don't really know what to wish for now (except HKT's 😛 ) |
weswigham commentedAug 2, 2019 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
exportconstHeaderMenuItem:<Eextendsobject=ReactAnchorAttr>(props:React.PropsWithChildren<HeaderMenuItemProps<E>>,ref:React.Ref<HTMLElement>):React.ReactElement; |
kalbert312 commentedAug 5, 2019
That works, but was hoping it was possible to do something with the interface so all future changes on that interface are propagated to my definition. Is it not possible? |
weswigham commentedAug 5, 2019
Not really, no. An interface simply can't have free type variables in the location you need for a generic component. |
bergerbo commentedNov 28, 2019
Hello@ahejlsberg ! I'm facing a weird issue regarding higher order function type inferance and I'd like your insight. I'm trying to type a compose function and can't get it to work, Here's what I have declarefunctioncompose4<Aextendsany[],B,C,D,E>(de:(d:D)=>E,cd:(c:C)=>D,bc:(b:B)=>C,ab:(...args:A)=>B,):(...args:A)=>E;constid=<T>(t:T)=>tconstcompose4id=compose4(id,id,id,id) The error is positionned on the second parameter On the other hand, the same function with parameters in piping order works fine : declarefunctionpipe4<Aextendsany[],B,C,D,E>(ab:(...args:A)=>B,bc:(b:B)=>C,cd:(c:C)=>D,de:(d:D)=>E,):(...args:A)=>E;constpipe4id=pipe4(id,id,id,id) Is this by design ? Do you think of any work around I might be able to use here ? Thanks again for everything ! |
bergerbo commentedNov 28, 2019
My bad found the related issue :#31738 |
maraisr commentedJan 8, 2020 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Further that, is there a reason why something this wouldnt work? constTest:<PayloadTypeextendsunknown,E=HTMLUListElement>(props:SuggestionProps<PayloadType>&RefAttributes<E>)=>ReactElement=forwardRef((props:SuggestionProps<PayloadType>,ref:Ref<E>)=>{return<h1>test</h1>;}); |
johnrom commentedJan 20, 2020 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
@maraisr constTest:<PayloadTypeextendsunknown,E=HTMLUListElement>(props:PayloadType&React.RefAttributes<E>)=>React.ReactElement=React.forwardRef(<PayloadTypeextendsunknown,E=HTMLUListElement>(props:PayloadType,ref:React.Ref<E>)=>{return<h1>test</h1>;});// ERROR: 'unknown' is assignable to the constraint of type 'PayloadType', but 'PayloadType' could be instantiated with a different subtype of constraint 'unknown'. So right now, at least as far as I can tell, the answer is to add It wouldn't be so bad if we could just ignore the specific error above, but sadly ts-ignore will ignore all issues. ref:#19139 |
tony commentedMay 4, 2020
Hi there! What commit is this merged in? It's difficult to find in the issue since there's a lot of mentions. Thanks |
DanielRosenwasser commentedMay 4, 2020
tony commentedMay 4, 2020
Thank you@DanielRosenwasser! |
mwstr commentedNov 16, 2022
Hello 👋. I'm trying to create a interfaceProviderHandler<Aextendsany[]=any[],R=any>{(this:RequestContext, ...args:A):R}typeFlatPromise<T>=Promise<TextendsPromise<inferE> ?E :T>interfaceWrapCall{<TextendsProviderHandler>(provider:Provider<T>):TextendsProviderHandler< inferA, inferR> ?(...args:A)=>FlatPromise<R> :never}interfaceRequestContext{/* ... */call:WrapCall}classProvider<TextendsProviderHandler=ProviderHandler>{handler:Tconstructor(options:{handler:T}){this.handler=options.handler}}constcontext={/* imagine it implemented */}asRequestContext Everything works great if handler is a simple function: constp1=newProvider({asynchandler(){return5}})// r1 = numberconstr1=awaitcontext.call(p1)() But it does not work with generic handlers: constp2=newProvider({asynchandler<T>(t:T){returnt}})// actual: r2 = unknown// expected: r2 = stringconstr2=awaitcontext.call(p2)('text') |
Uh oh!
There was an error while loading.Please reload this page.
With this PR we enable inference of generic function type results for generic functions that operate on other generic functions. For example:
Previously,
listBoxwould have type(x: any) => { value: any[] }with an ensuing loss of type safety downstream.boxListandflippedwould likewise have typeanyin place of type parameters.When an argument expression in a function call is of a generic function type, the type parameters of that function type are propagated onto the result type of the call if:
For example, in the call
as the arguments are processed left-to-right, nothing has been inferred for
AandBupon processing thelistargument. Therefore, the type parameterTfromlistis propagated onto the result ofpipeand inferences are made in terms of that type parameter, inferringTforAandT[]forB. Theboxargument is then processed as before (because inferences exist forB), using the contextual typeT[]forVin the instantiation of<V>(x: V) => { value: V }to produce(x: T[]) => { value: T[] }. Effectively, type parameter propagation happens only when we would have otherwise inferred the constraints of the called function's type parameters (which typically is the dreaded{}).The above algorithm is not a complete unification algorithm and it is by no means perfect. In particular, it only delivers the desired outcome when types flow from left to right. However, this has always been the case for type argument inference in TypeScript, and it has the highly desired attribute of working well as code is being typed.
Note that this PR doesn't change our behavior for contextually typing arrow functions. For example, in
we infer
any(the constraint declared by thepipefunction) as we did before. We'll infer a generic type only if the arrow function explicitly declares a type parameter:When necessary, inferred type parameters are given unique names:
Above, we rename the second
TtoT1in the last example.Fixes#417.
Fixes#3038.
Fixes#9366.
Fixes#9949.