镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Suggestion: Enable inferring the result type of a generic function based on the context in which it is used #3423

Description

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)

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[]

Case 2: Empty container constructors:

function toArrayOf<a>() : a[] { return []; }
function id<a>(value: a) : a { return value; }
function withSome<a>(valueOpt: a, haveSome: (value: a) => r, haveNone: (reason: string)  => r) : r {
    return valueOpt != null ? haveSome(valueOpt) : haveNone('Value is unspecified.');
}
var valuesOpt = Math.random() > 0.5 ? ['a', 'b', 'c'] : null;
var values = withSome(valueOpt, id, toArrayOf); // <-- actual result is {}, expected is string[]

Activity

  1. zpdDG4gta8XKpMCd commented on Jun 9, 2015

    @zpdDG4gta8XKpMCd
    Author

    Case 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>
  2. mhegazy commented on Jun 9, 2015

    @mhegazy
    Contributor

    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:

    function fromNone(reason: string) { return { none: reason }; }
    var value = 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>.

  3. zpdDG4gta8XKpMCd commented on Jun 10, 2015

    @zpdDG4gta8XKpMCd
    Author
    • 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:
    interface Optional<a> {
       'an optional': Optional<a>; // a brand to get a nominal type: https://github.com/proxy/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 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 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

    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#

  4. JsonFreeman commented on Aug 5, 2015

    @JsonFreeman
    Contributor

    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.

  5. zpdDG4gta8XKpMCd commented on Aug 6, 2015

    @zpdDG4gta8XKpMCd
    Author

    Jason Freeman (@JsonFreeman)

    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.

  6. JsonFreeman commented on Aug 6, 2015

    @JsonFreeman
    Contributor

    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:

    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 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): {}.

  7. zpdDG4gta8XKpMCd commented on Mar 4, 2016

    @zpdDG4gta8XKpMCd
    Author

    guys, i am dying to see this on your upcoming design meeting agenda, please please please

  8. RyanCavanaugh commented on Mar 4, 2016

    @RyanCavanaugh
    Member

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.SuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions