Repository navigation
Suggestion: Enable inferring the result type of a generic function based on the context in which it is used #3423
Description
Activity
- addedSuggestionAn idea for TypeScriptAn idea for TypeScriptNeeds ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.This issue needs a plan that clarifies the finer details of how it could be implemented.
on Jun 8, 2015 zpdDG4gta8XKpMCd commented
on Jun 9, 2015 AuthorMore actionsCase 3: Sum types (empty container sort of case)
interface Optional<a> { some?: a; none?: string; } function fromSome<a>(value: a) : Optional<a> { return { some: value }; } function fromNone<a>(reason: string): Optional<a> { return { none: reason }; } var value = Math.random() > 0.5 ? fromSome(123) : fromNone('Not this time'); // <-- acutual Optional<{}> expected Optional<number>
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:
function fromNone(reason: string) { return { none: reason }; } var value = Math.random() > 0.5 ? fromSome(123) : fromNone('Not this time');
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 commented
on Jun 10, 2015 AuthorMore actionsfromNoneis a constructor for theOptional<a>type, it has to be generic in order to produceOptional<a>- you are right that the
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:
interface Optional<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 } function fromSome<a>(value: a) : Optional<a> { return <any> { some: value }; } function fromNone<a>(reason: string) : Optional<a> { return <any> { none: reason }; }
- sugar or not the whole point of the
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:
// with type omitted interface Optional<a> { some?: a; none?: string; } function fromNone(reason: string) { return { none: reason; } var noUser = fromNone('There is no user you are looking for.'); var noAccount = 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 preserved interface Optional<a> { 'uses a': a; } function fromNone<a>(reason: string) : Optional<a> { return <any>{ none: reason; } } var noUser = fromNone<User>('There is no user you are looking for.'); var noAccount = fromNone<Account>('There is no account either,'); noUser = noAccount; // <-- disallowed as it should be, since doesn't make sense
- i am surprised to see you clueless as for where i am coming from, take a look at Options in F#: https://msdn.microsoft.com/en-us/library/dd233245.aspx they are generic and they have both constructors for the
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.aspx
basically 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 commented
on Aug 5, 2015 ContributorMore actionsAleksey-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 commented
on Aug 6, 2015 AuthorMore actionsI 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.
JsonFreeman commented
on Aug 6, 2015 ContributorMore actionsThis is a pretty concise summary of unification with an example:
https://cs.brown.edu/courses/cs173/2002/Lectures/2002-11-13.pdfMore 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:
function fail<r>(message: string) : r { throw new Error(message); } function id<a>(value: a) : a { return value; } function withFew<a, r>(values: a[], haveFew: (values: a[]) => r, haveNone: (reason: string) => r) : r { return values.length > 0 ? haveFew(values) : haveNone('Array is empty.'); } var values = Math.random() > 0.5 ? ['a', 'b', 'c'] : []; var few = 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
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 commented
on Mar 4, 2016 AuthorMore actionsguys, i am dying to see this on your upcoming design meeting agenda, please please please
Reacted by Amir AradRyanCavanaugh commented
on Mar 4, 2016 MemberMore actionsWe 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.
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)
Case 2: Empty container constructors: