microsoft/TypeScript

Supporting generic type inference over the other higher-order functions

falsandtru opened this issue ยท 14 comments

Currently, TypeScript infers generic types when referring its type immediately. Its behavior is undesirable for using higher-order functions. The type inference of generics should delay until it will be called.

function flip<a, b, c>(f: (a: a, b: b) => c): (b: b, a: a) => c {
  return (b: b, a: a) => f(a, b);
}
function zip<T, U>(x: T, y: U): [T, U] {
  return [x, y];
}
var expected: <T, U>(y: U, x: T) => [T, U] = flip(zip);
var actual: (y: {}, x: {}) => [{}, {}] = flip(zip);

this issue is really problematic for functional paradigm users :

const map = <T, U>(transform: (t: T) => U) =>
    (arr: T[]) => arr.map(transform)

const identity = <T>(t: T) => t;

const identityStr = (t: string) => t;


const arr: string[] = map(identityStr)(['a']);
const arr1: string[] = map(identity)(['a']); // Type '{}[]' is not assignable to type 'string[]'.

(welcome back @fdecampredon!)

(thanks ! :) )

baio commented

Yep, there is to many <any> in my Ramda code.

#10247, #9949 for the reference. And see @gcnew's comment

Relevant paper: "Practical type inference for higher-rank types". Specifically the section on subsumption of parametrically polymorphic signatures.

Ultimately this comes down to better unification, which is what @gcnew has done some work on if you follow along in #9949. For the flip example above, the rank 1 function type <T, U>(x: T, y: U) => [T, U] can be substituted for the rank 0 function type (a: a) => (b: b) => c to obtain f: (x: a, y: b) => [a, b], because the former is more polymorphic than the latter. From there we need TypeScript's usual approach to unification to kick in and infer c = [a, b]. The tricky part is coming up with a general algorithm to do all of this.

Another example:

const compose = <A, B, C>(g: (b: B) => C, f: (a: A) => B) => (a: A) => g(f(a))
const identity = <T>(x: T) => x
const lessThanTen = (x: number) => x < 10
const composed = compose(lessThanTen, identity)
//                       ^^^^^^^^^^^
// Argument of type '(x: number) => boolean' is not assignable to parameter of type '(b: {}) => boolean'.
//   Types of parameters 'x' and 'b' are incompatible.
//     Type '{}' is not assignable to type 'number'.

Can be fixed with explicit type parameters

const composed = compose<number, number, boolean>(lessThanTen, identity)

but would be really nice if this could just unify!

I would love to know if there is some movement here. Here's another usecase: https://stackoverflow.com/q/47934804/592641

I've ran into this issue when using lodash's memoize function when the function being memoised is generic.

This would also be really valuable for mixins on react components with proptypes

export class MyComponent extends MyMixin(React.Component<MyComponentProps>) {}

I love the new mixins feature since 2.2 and I was really excited about refactoring my code to create a variety of mixins for my components until I realized as of right now it won't quite work as I'd hoped without a lot of manual type declaring ๐Ÿ˜ญ

Heads up I put up my attempt at implementing this at #24626. No idea if it'll go anywhere but it works for a lot of the cases I've tried so feel free to try it out if you're interested.

@kpdonn ๐Ÿ˜ญ It's so beautiful.

Any progress on this?

Higher order function type inference now implemented in #30215.