Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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

Merged
ahejlsberg merged 20 commits intomasterfromhigherOrderFunctionTypeInference
Mar 8, 2019

Conversation

@ahejlsberg
Copy link
Member

@ahejlsbergahejlsberg commentedMar 4, 2019
edited
Loading

With this PR we enable inference of generic function type results for generic functions that operate on other generic functions. For example:

declarefunctionpipe<Aextendsany[],B,C>(ab:(...args:A)=>B,bc:(b:B)=>C):(...args:A)=>C;declarefunctionlist<T>(a:T):T[];declarefunctionbox<V>(x:V):{value:V};constlistBox=pipe(list,box);// <T>(a: T) => { value: T[] }constboxList=pipe(box,list);// <V>(x: V) => { value: V }[]constx1=listBox(42);// { value: number[] }constx2=boxList("hello");// { value: string }[]constflip=<A,B,C>(f:(a:A,b:B)=>C)=>(b:B,a:A)=>f(a,b);constzip=<T,U>(x:T,y:U):[T,U]=>[x,y];constflipped=flip(zip);// <T, U>(b: U, a: T) => [T, U]constt1=flipped(10,"hello");// [string, number]constt2=flipped(true,0);// [number, boolean]

Previously,listBox would have type(x: any) => { value: any[] } with an ensuing loss of type safety downstream.boxList andflipped would likewise have typeany in 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:

  • the called function is a generic function that returns a function type with a single call signature,
  • that single call signature doesn't itself introduce type parameters, and
  • in the left-to-right processing of the function call arguments, no inferences have been made for any of the type parameters referenced in the contextual type for the argument expression.

For example, in the call

constf=pipe(list,box);

as the arguments are processed left-to-right, nothing has been inferred forA andB upon processing thelist argument. Therefore, the type parameterT fromlist is propagated onto the result ofpipe and inferences are made in terms of that type parameter, inferringT forA andT[] forB. Thebox argument is then processed as before (because inferences exist forB), using the contextual typeT[] forV in 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

constf=pipe(x=>[x],box);// (x: any) => { value: any[] }

we inferany (the constraint declared by thepipe function) as we did before. We'll infer a generic type only if the arrow function explicitly declares a type parameter:

constf=pipe(<U>(x:U)=>[x],box);// <U>(x: U) => { value: U[] }

When necessary, inferred type parameters are given unique names:

declarefunctionpipe2<A,B,C,D>(ab:(a:A)=>B,cd:(c:C)=>D):(a:[A,C])=>[B,D];constf1=pipe2(list,box);// <T, V>(a: [T, V]) => [T[], { value: V }]constf2=pipe2(box,list);// <V, T>(a: [V, T]) => [{ value: V }, T[]]constf3=pipe2(list,list);// <T, T1>(a: [T, T1]) => [T[], T1[]]

Above, we rename the secondT toT1 in the last example.

Fixes#417.
Fixes#3038.
Fixes#9366.
Fixes#9949.

j-oliveras, schrepfler, dragomirtitian, KrisJordan, bartosz-k, benlesh, yortus, falsandtru, jkup, danielyogel, and 241 more reacted with thumbs up emojifvilante, KiaraGrouwstra, arturkulig, j-oliveras, zpdDG4gta8XKpMCd, xiaoxiangmoe, psxcode, HerringtonDarkholme, Kingwl, BANG88, and 18 more reacted with laugh emojiksaldana1, punmechanic, dubzzz, giogonzo, pelotom, j-oliveras, schrepfler, cartant, dragomirtitian, KrisJordan, and 116 more reacted with hooray emojij-oliveras, schrepfler, wrumsby, felixrieseberg, samiskin, KrisJordan, kristian-puccio, Birowsky, benlesh, jkup, and 114 more reacted with heart emojiBnaya, nateabele, karol-majewski, NikChao, cartant, sinisterstumble, JozefFlakus, wthinkit, fvilante, KiaraGrouwstra, and 46 more reacted with rocket emojipsxcode, zry656565, Kingwl, j-oliveras, AbraaoAlves, vyorkin, paavohuhtala, 0ubbe, another-guy, prism4time, and 3 more reacted with eyes emoji
@jack-williams
Copy link
Collaborator

jack-williams commentedMar 4, 2019
edited
Loading

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{} would have been inferred it might not be because the application is sufficiently generic, rather inference didn't produce anything sensible but that still type-checked.

I haven't looked into this much, so apologies in advance if I've got something wrong.

Xayne reacted with thumbs up emoji

@ahejlsberg
Copy link
MemberAuthor

@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{} instead ofnumber), but now what was a loss of type safety instead has become an error. Ideally we ought to first infer from arguments that are not generic functions and then process the generic functions afterwards, similar to what we do for contextually typed arrow functions and function expressions.

@jack-williams
Copy link
Collaborator

jack-williams commentedMar 5, 2019
edited
Loading

@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.

Xayne reacted with thumbs up emoji

@RyanCavanaugh
Copy link
Member

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]);
HerringtonDarkholme and silouone reacted with laugh emoji

@jack-williams
Copy link
Collaborator

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{} type downstream (either safely, or not), will now have to fix upstream breaks. I have no idea whether this is a practical problem, though.

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 isArgument of type '10' is not assignable to parameter of type 'A'. I wonder if there is a way to mark the parameterA as rigid and when relating to that type, suggest that the user given an instantiation for the call, and say that inference flows left-to-right.

RyanCavanaugh and mkantor reacted with thumbs up emoji

@KiaraGrouwstra
Copy link
Contributor

@ikatyang@Aleksey-Bykov@whitecolor@HerringtonDarkholme@aluanhaddad@ccorcos@gcnew@goodmind

RyanCavanaugh and fvilante reacted with laugh emojizpdDG4gta8XKpMCd, ikatyang, HerringtonDarkholme, ccorcos, gcnew, fvilante, and trusktr reacted with hooray emojizpdDG4gta8XKpMCd, ikatyang, HerringtonDarkholme, LukeSheard, and fvilante reacted with heart emojiikatyang, zpdDG4gta8XKpMCd, HerringtonDarkholme, and fvilante reacted with rocket emoji

@zpdDG4gta8XKpMCd
Copy link

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 😛 )

c69, dewey92, ddoronin, landonpoch, steida, ShanonJackson, chrisfls, jacobmadsen, unao, fvilante, and 2 more reacted with thumbs up emojic69, steida, and fvilante reacted with laugh emoji

@weswigham
Copy link
Member

weswigham commentedAug 2, 2019
edited
Loading

RefForwardingComponent declares its type parameters as static, but for a generic component, you need type parameters on the signature, ergo you make your componentlook like a ref forwarding component (and it will be compatible with one), but not actually inherit from the declared shape:

exportconstHeaderMenuItem:<Eextendsobject=ReactAnchorAttr>(props:React.PropsWithChildren<HeaderMenuItemProps<E>>,ref:React.Ref<HTMLElement>):React.ReactElement;
kalbert312, jbr, keshavkaul, and rickstaa reacted with thumbs up emoji

@kalbert312
Copy link

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
Copy link
Member

Not really, no. An interface simply can't have free type variables in the location you need for a generic component.

@bergerbo
Copy link

Hello@ahejlsberg !
Thanks for your work it's of great help !

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,
it's a mere copy of your pipe function, only with the parameters in reverse order.
Not only does it not infer, it doesn't even compile !

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

Argument of type '<T>(t: T) => T' is not assignable to parameter of type '(c: T1) => T'.  Type 'T1' is not assignable to type 'T'.    'T1' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '{}'.(2345)

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
Copy link

My bad found the related issue :#31738

@maraisr
Copy link
Member

maraisr commentedJan 8, 2020
edited
Loading

RefForwardingComponent declares its type parameters as static, but for a generic component, you need type parameters on the signature, ergo you make your componentlook like a ref forwarding component (and it will be compatible with one), but not actually inherit from the declared shape:

exportconstHeaderMenuItem:<Eextendsobject=ReactAnchorAttr>(props:React.PropsWithChildren<HeaderMenuItemProps<E>>,ref:React.Ref<HTMLElement>):React.ReactElement;

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>;});

cc@weswigham

@johnrom
Copy link

johnrom commentedJan 20, 2020
edited
Loading

@maraisrPayloadType is not available in the right side of the assignment, it's only available within the type definition itself. You could also define it as a generic to your inner component, but thenPayloadType1 will not be assignable toPayloadType2 because of this error:

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// @ts-ignore and hope the types never change. The reason being that the error is actually not applicable here as we are guaranteeing that PayloadType will not be instantiated with a different subtype. UsingTest will then work exactly how you expect it to in TS.

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
Copy link

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
Copy link
Member

TypeScript 3.4:https://github.com/Microsoft/TypeScript/wiki/Roadmap#34-march-2019

tony and rickstaa reacted with thumbs up emoji

@tony
Copy link

tony commentedMay 4, 2020

Thank you@DanielRosenwasser!

@mwstr
Copy link

Hello 👋. I'm trying to create aProvider class that stores a handler function and can be executed with some context:

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')

Playground link

@microsoftmicrosoft locked asresolvedand limited conversation to collaboratorsOct 21, 2025
Sign up for freeto subscribe to this conversation on GitHub. Already have an account?Sign in.

Reviewers

@weswighamweswighamweswigham left review comments

@RyanCavanaughRyanCavanaughRyanCavanaugh approved these changes

@sandersnsandersnAwaiting requested review from sandersn

@DanielRosenwasserDanielRosenwasserAwaiting requested review from DanielRosenwasser

Assignees

@ahejlsbergahejlsberg

Labels

None yet

Projects

None yet

Milestone

No milestone

20 participants

@ahejlsberg@jack-williams@RyanCavanaugh@KiaraGrouwstra@zpdDG4gta8XKpMCd@typescript-bot@mweststrate@j-oliveras@maciejw@AnyhowStep@mivanovaxway@thupi@weswigham@kalbert312@bergerbo@maraisr@johnrom@tony@DanielRosenwasser@mwstr

[8]ページ先頭

©2009-2025 Movatter.jp