Uh oh!
There was an error while loading. Please reload this page.
Higher order function type inference - #30215
Conversation
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. |
Anders Hejlsberg (ahejlsberg)
commented
Mar 4, 2019
Jack Williams (@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 |
Anders Hejlsberg (@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. |
Ryan Cavanaugh (RyanCavanaugh)
commented
Mar 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 (jack-williams)
commented
Mar 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");// errorIt'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 a long 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 |
ZpdDG4gta (zpdDG4gta8XKpMCd)
commented
Mar 5, 2019
best day ever! thank you so much Anders Hejlsberg (@ahejlsberg) and the team you made my day! i don't really know what to wish for now (except HKT's 😛 ) |
exportconstHeaderMenuItem: <Eextendsobject=ReactAnchorAttr>(props: React.PropsWithChildren<HeaderMenuItemProps<E>>,ref: React.Ref<HTMLElement>): React.ReactElement; |
Kyle Albert (kalbert312)
commented
Aug 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? |
Wesley Wigham (weswigham)
commented
Aug 5, 2019
Not really, no. An interface simply can't have free type variables in the location you need for a generic component. |
bergerbo
commented
Nov 28, 2019
Hello Anders Hejlsberg (@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
commented
Nov 28, 2019
My bad found the related issue : #31738 |
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>;}); |
Marais Rossouw (@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 Narlock (tony)
commented
May 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 |
Tony Narlock (tony)
commented
May 4, 2020
Thank you Daniel Rosenwasser (@DanielRosenwasser)! |
Meowster (mwstr)
commented
Nov 16, 2022
Hello 👋. I'm trying to create a interfaceProviderHandler<Aextendsany[]=any[],R=any>{(this: RequestContext, ...args: A): R}typeFlatPromise<T>=Promise<TextendsPromise<infer E> ? E : T>interfaceWrapCall{<TextendsProviderHandler>(provider: Provider<T>): TextendsProviderHandler<
infer A,
infer R>
? (...args: A)=>FlatPromise<R>
: never}interfaceRequestContext{/* ... */call: WrapCall}classProvider<TextendsProviderHandler=ProviderHandler>{handler: Tconstructor(options: {handler: T}){this.handler=options.handler}}constcontext={/* imagine it implemented */}asRequestContextEverything 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') |
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.