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