Skip to content
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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
zpdDG4gta8XKpMCd opened this issue Jun 8, 2015 · 8 comments
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@zpdDG4gta8XKpMCd
Copy link

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[]
@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Jun 8, 2015
@zpdDG4gta8XKpMCd
Copy link
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>

@mhegazy
Copy link
Contributor

mhegazy commented Jun 9, 2015

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

@zpdDG4gta8XKpMCd
Copy link
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://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#

@JsonFreeman
Copy link
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 @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
Copy link
Author

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

@zpdDG4gta8XKpMCd
Copy link
Author

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

@RyanCavanaugh
Copy link
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
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

4 participants