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

Inferring a generic should put the type variable in the inner scope when possible #49505

Open
5 tasks done
jsoldi opened this issue Jun 12, 2022 · 1 comment
Open
5 tasks done
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@jsoldi
Copy link

jsoldi commented Jun 12, 2022

Suggestion

πŸ” Search Terms

  • generics
  • polymorphism
  • type inference

βœ… Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

When inferring a generic type, put the type variable in the inner scope as long as it's possible to make the result as general as possible.

πŸ“ƒ Motivating Example

Say I have a simple composition function:

declare const compose: <A, B, C>(f: (a: A) => B, g: (b: B) => C) => (a: A) => C

The current version of TS allows me to do this and get the correct type:

const comp = compose(<T>(t: T) => t, <T>(t: T) => t); // comp: <T>(a: T) => T

But say I want to wrap the result in an object:

declare const composeObj: <A, B, C>(f: (a: A) => B, g: (b: B) => C) => { value: (a: A) => C }

Now the generic is gone, and comp above is typed as { value: (a: unknown) => unknown }.

I think what's happening is that TS is trying to put the T variable outside the scope of the object because if, instead of a wrapping object, I use a wrapping function, this is what happens:

declare const composeFun: <A, B, C>(f: (a: A) => B, g: (b: B) => C) => () => (a: A) => C
const comp = composeFun(<T>(t: T) => t, <T>(t: T) => t); // comp: <T>() => (a: T) => T

TS is putting T on the outer level, so if I call comp() the result will be (a: unknown) => unknown. A better result would've been to get () => <T>(a: T) => T. This way, calling comp() would not discard the type variable and, if I'm not mistaken, the result would be a more general type than the result we currently get, without breaking any typing rules.

πŸ’» Use Cases

Being able to wrap generic functions when they're the result of type inference. My specific case was writing a type guarding/converting utility using a wrapper around functions of the form a -> Maybe<b> that can be chained together by the dot operator to represent function composition, such that types can be further specified. Something like Thing.isPrimitive.isNumber.isInteger. But this is not possible without the possibility to wrap the result of my compose function.

@jcalz
Copy link
Contributor

jcalz commented Jun 13, 2022

Related to #30215, which implemented the higher order function inference that makes your first compose work, and describes how such inference only happens in very specific circumstances (which your composeObj does not satisfy because it does not return a function, but an object with a method)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

3 participants