You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Suggestion: Enable inferring the result type of a generic function based on the context in which it is used #3423
It's a very common situation in our code when we wish TypeScript could infer the result type of a function with a generic result based on the context in which that function is used. Here are a couple of use cases to support the feature request:
Case 1: Generic fail function (for being able to throw from an expression)
functionfail<r>(message: string) : r{thrownewError(message);}functionid<a>(value: a) : a{returnvalue;}functionwithFew<a,r>(values: a[],haveFew: (values: a[])=>r,haveNone: (reason: string)=>r) : r{returnvalues.length>0 ? haveFew(values) : haveNone('Array is empty.');}varvalues=Math.random()>0.5 ? ['a','b','c'] : [];varfew=withFew(values,id,fail);// <-- actual result is {}, expected is string[]
Case 2: Empty container constructors:
functiontoArrayOf<a>() : a[]{return[];}functionid<a>(value: a) : a{returnvalue;}functionwithSome<a>(valueOpt: a,haveSome: (value: a)=>r,haveNone: (reason: string)=>r) : r{returnvalueOpt!=null ? haveSome(valueOpt) : haveNone('Value is unspecified.');}varvaluesOpt=Math.random()>0.5 ? ['a','b','c'] : null;varvalues=withSome(valueOpt,id,toArrayOf);// <-- actual result is {}, expected is string[]
fromNone should not be defined as generic. Generic type parameter a has no manifestation in the argument, and in return has no runtime impact. there is no way for function fromNone to observe the type a at runtime; and i believe this is the same issue with other examples as well.. the intended type flow is rather artificial.
the call to fromNone<number>('string') is just sugar for a cast <Optional<number>> fromNone('string'); I do not see how the compiler should figure this type out from use cases?
I think a better typing would not make fromNone gneric:
functionfromNone(reason: string){return{none: reason};}varvalue=Math.random()>0.5 ? fromSome(123) : fromNone('Not this time');
this way value here is Optional<number>|{none:string}, which is correct representation of the two values. the only property that is exists in both branches is none?: string. contextual type can be used then to make value: Optional<number>.
fromNone is a constructor for the Optional<a> type, it has to be generic in order to produce Optional<a>
you are right that the a parameter is no use in fromNone function, i deliberately simplified the example for better clarity, the original definitions that we use in production code look that this:
interfaceOptional<a>{'an optional': Optional<a>;// a brand to get a nominal type: https://gist.github.com/aleksey-bykov/0ab85f0b5e83fc848f85'depends on a': a;// an explicit yet nominal use of `a` to make it a part of the object surface: https://github.com/Microsoft/TypeScript/issues/468}functionfromSome<a>(value: a) : Optional<a>{return<any>{some: value};}functionfromNone<a>(reason: string) : Optional<a>{return<any>{none: reason};}
sugar or not the whole point of the none case is to be able to encode a type which is missing a value, your suggestion about a uion with {none:string} keeps only a missing value while discards the information about the type itself, but the type cannot be thrown away because the whole purpose for having it in the first place is to enforce a set of permitted operations, consider:
// with type omittedinterfaceOptional<a>{some?: a;none?: string;}functionfromNone(reason: string){return{none: reason;}varnoUser=fromNone('There is no user you are looking for.');varnoAccount=fromNone('There is no account either.');noUser=noAccount;// since no type is in place the assignment is legit albeit doesn't make sense// with type preservedinterfaceOptional<a>{'uses a': a;}functionfromNone<a>(reason: string) : Optional<a>{return<any>{none: reason;}}varnoUser=fromNone<User>('There is no user you are looking for.');varnoAccount=fromNone<Account>('There is no account either,');noUser=noAccount;// <-- disallowed as it should be, since doesn't make sense
Aleksey-Bykov I think what you are asking for is a persistent bottom type that will be inferred when there are no inference candidates, and will be subsumed by all other types. Similar to null or undefined, except that it would not widen to any, or at least it would not widen at the end of type argument inference.
Your suggestion to infer from the context would work for case 1 and case 2, but not case 3 because case 3 has no contextual type, it's just a union.
But actually I think Mohamed Hegazy (@mhegazy) is right that these functions should not generic. What you really want is for them to return some bottom type that will not widen.
Aleksey-Bykov I will also add that many of the cases you are mentioning suggest that you are trying to do type unification in TypeScript. The inference system in TypeScript is not unification based. Unification treats all occurrences of a type as equal citizens when it comes to inference, even if the direction of inference does not match the direction of the flow of values. By contrast, TypeScript's inference is direction sensitive. In only infers types in the direction that matches the value flow. This is why it infers from parameters, but not return types. It is the same reason TypeScript does not infer the type of a value based on how the value is used later on.
I think what you are asking for is a persistent bottom type that will be inferred when there are no inference candidates
Very well could be, although my thought was that the return types in such situation can only be inferred when there is at least one candidate whose type would be the type of the result.
Similar to null or undefined, except that it would not widen to any, or at least it would not widen at the end of type argument inference.
I wish at this point I could say a definite yes or no, but I don't know the domain subject so well so I have to trust someone who claims to know it better.
... but not case 3 because case 3 has no contextual type, it's just a union.
Can't see how case 3 is different from say case 2. Anyway with new cool features of TS 1.6 (with parameters in type aliases and custom type guards) case 3 can already be taken down as irrelevant.
But actually I think Mohamed Hegazy (@mhegazy) is right that these functions should not generic. What you really want is for them to return some bottom type that will not widen.
Indeed using a specific case of a union (a sum term) rather than a all-cases-considered container (a product) eliminates a need to for each case to be dependent on each other's case type parameters.
I will also add that many of the cases you are mentioning suggest that you are trying to do type unification in TypeScript.
This is where I will be frowning. I don't have enough theoretical background to reason about whether what you just said is right or wrong. What is a type unification? Is there a link that lays it down in plain English? Similarly what is "direction sensitive" inference? Is it good or bad?
I have a couple of very practical situations at hands that I wish required less typing (were inferred by the language). I hope you are not going to say it is not possible to do due to fundamental limitations that TypeScript design team has decided to embrace.
More to the point: Let's take case 1 that you've given (case 2 is similar). I'm going to copy it here for ease of looking:
functionfail<r>(message: string) : r{thrownewError(message);}functionid<a>(value: a) : a{returnvalue;}functionwithFew<a,r>(values: a[],haveFew: (values: a[])=>r,haveNone: (reason: string)=>r) : r{returnvalues.length>0 ? haveFew(values) : haveNone('Array is empty.');}varvalues=Math.random()>0.5 ? ['a','b','c'] : [];varfew=withFew(values,id,fail);// <-- actual result is {}, expected is string[]
My claim is that there is no useful inference that can be made from the fail function in the call to withFew. You want string[] to be inferred, but fail says nothing about string[]. The only way to infer string[] is by inferring it from id, which indeed we do. This is because id says that it returns the same type it is given.
If I understand correctly, you are suggesting that withFew would provide a context that makes the r in fail correspond to string[]. Then the instantiated signature for fail would be (message: string): string[], and then we could infer string[] back to the r in withFew. But that becomes rather meaningless because fail could not tell withFew anything that the latter didn't already know.
The real problem here is that an inference with no candidates yields {}, which spoils the inference for the call to withFew. If just the id function were used for inference, we'd be in good shape. But fail is providing an extra candidate {} which comes to ruin the party. This is actually the problem that needs to be solved.
And making fail generic does not actually do anything. It is just as if you had written function fail(message: string): {}.
We generally don't bring Needs Proposal issues to the meeting. If you can figure out a way to solve this problem, we can take a closer look. Otherwise extant problems around more common patterns take higher priority in terms of us figuring out how to fix things.
Activity
zpdDG4gta8XKpMCd commentedon Jun 9, 2015
Case 3: Sum types (empty container sort of case)
mhegazy commentedon Jun 9, 2015
fromNoneshould not be defined as generic. Generic type parameterahas no manifestation in the argument, and in return has no runtime impact. there is no way for functionfromNoneto observe the typeaat runtime; and i believe this is the same issue with other examples as well.. the intended type flow is rather artificial.the call to
fromNone<number>('string')is just sugar for a cast<Optional<number>> fromNone('string'); I do not see how the compiler should figure this type out from use cases?I think a better typing would not make fromNone gneric:
this way
valuehere isOptional<number>|{none:string}, which is correct representation of the two values. the only property that is exists in both branches isnone?: string. contextual type can be used then to makevalue: Optional<number>.zpdDG4gta8XKpMCd commentedon Jun 10, 2015
fromNoneis a constructor for theOptional<a>type, it has to be generic in order to produceOptional<a>aparameter is no use infromNonefunction, i deliberately simplified the example for better clarity, the original definitions that we use in production code look that this:nonecase is to be able to encode a type which is missing a value, your suggestion about a uion with{none:string}keeps only a missing value while discards the information about the type itself, but the type cannot be thrown away because the whole purpose for having it in the first place is to enforce a set of permitted operations, consider:someandnonecases and if you take a closer look at thenonecase you can see it is generic too https://msdn.microsoft.com/en-us/library/ee340390.aspxbasically any FP languge has it, and i just wish TypeScript had some of its features too and wasn't that much of OOP and C#
JsonFreeman commentedon Aug 5, 2015
Aleksey-Bykov I think what you are asking for is a persistent bottom type that will be inferred when there are no inference candidates, and will be subsumed by all other types. Similar to null or undefined, except that it would not widen to
any, or at least it would not widen at the end of type argument inference.Your suggestion to infer from the context would work for case 1 and case 2, but not case 3 because case 3 has no contextual type, it's just a union.
But actually I think Mohamed Hegazy (@mhegazy) is right that these functions should not generic. What you really want is for them to return some bottom type that will not widen.
Aleksey-Bykov I will also add that many of the cases you are mentioning suggest that you are trying to do type unification in TypeScript. The inference system in TypeScript is not unification based. Unification treats all occurrences of a type as equal citizens when it comes to inference, even if the direction of inference does not match the direction of the flow of values. By contrast, TypeScript's inference is direction sensitive. In only infers types in the direction that matches the value flow. This is why it infers from parameters, but not return types. It is the same reason TypeScript does not infer the type of a value based on how the value is used later on.
zpdDG4gta8XKpMCd commentedon Aug 6, 2015
Jason Freeman (@JsonFreeman)
Very well could be, although my thought was that the return types in such situation can only be inferred when there is at least one candidate whose type would be the type of the result.
I wish at this point I could say a definite yes or no, but I don't know the domain subject so well so I have to trust someone who claims to know it better.
Can't see how case 3 is different from say case 2. Anyway with new cool features of TS 1.6 (with parameters in type aliases and custom type guards) case 3 can already be taken down as irrelevant.
Indeed using a specific case of a union (a sum term) rather than a all-cases-considered container (a product) eliminates a need to for each case to be dependent on each other's case type parameters.
This is where I will be frowning. I don't have enough theoretical background to reason about whether what you just said is right or wrong. What is a type unification? Is there a link that lays it down in plain English? Similarly what is "direction sensitive" inference? Is it good or bad?
I have a couple of very practical situations at hands that I wish required less typing (were inferred by the language). I hope you are not going to say it is not possible to do due to fundamental limitations that TypeScript design team has decided to embrace.
JsonFreeman commentedon Aug 6, 2015
This is a pretty concise summary of unification with an example:
https://cs.brown.edu/courses/cs173/2002/Lectures/2002-11-13.pdf
More to the point: Let's take case 1 that you've given (case 2 is similar). I'm going to copy it here for ease of looking:
My claim is that there is no useful inference that can be made from the
failfunction in the call towithFew. You wantstring[]to be inferred, butfailsays nothing aboutstring[]. The only way to inferstring[]is by inferring it fromid, which indeed we do. This is becauseidsays that it returns the same type it is given.If I understand correctly, you are suggesting that
withFewwould provide a context that makes therinfailcorrespond tostring[]. Then the instantiated signature forfailwould be(message: string): string[], and then we could inferstring[]back to therinwithFew. But that becomes rather meaningless becausefailcould not tellwithFewanything that the latter didn't already know.The real problem here is that an inference with no candidates yields
{}, which spoils the inference for the call towithFew. If just theidfunction were used for inference, we'd be in good shape. Butfailis providing an extra candidate{}which comes to ruin the party. This is actually the problem that needs to be solved.And making
failgeneric does not actually do anything. It is just as if you had writtenfunction fail(message: string): {}.zpdDG4gta8XKpMCd commentedon Mar 4, 2016
guys, i am dying to see this on your upcoming design meeting agenda, please please please
RyanCavanaugh commentedon Mar 4, 2016
We generally don't bring Needs Proposal issues to the meeting. If you can figure out a way to solve this problem, we can take a closer look. Otherwise extant problems around more common patterns take higher priority in terms of us figuring out how to fix things.