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

Why does TypeScript not infer types for some piped functions? #22051

Closed
ivan7237d opened this issue Feb 20, 2018 · 7 comments
Closed

Why does TypeScript not infer types for some piped functions? #22051

ivan7237d opened this issue Feb 20, 2018 · 7 comments

Comments

@ivan7237d
Copy link

This is a question rather than a bug report or a feature request, but I've tried asking this on stackoverflow and gitter with no success, so maybe I'll have more luck here.

In the following code snippet, if you look at the last two lines, TypeScript shows an error in the first one, and correctly infers types in the second one, although the difference is only the order in which the functions are piped.

const pipe = <A, B, C>(
  x: A,
  a: (x: A) => B, 
  b: (x: B) => C,
) => b(a(x));

// This just calls the function passed as argument.
const call = <A, B>(f: (x: A) => B) => (x: A) => f(x)

const a = pipe(1, x => x + 1, call(x => x + 1));
const b = pipe(1, call(x => x + 1), x => x + 1);

I use TypeScript 2.7.1 in the strict mode (including strictFunctionTypes), however the strict mode doesn't seem to matter here. Here is this snippet on TypeScript playground.

It is a problem that I often run into when working with RxJS, since in RxJS there is a similar pipe method and I pass arrow functions to it when using creation operators (like obs => merge(obs, otherObs)). Usually it's easy to work around this problem by specifying the argument type, but I would like to understand the logic behind this. Why is TypeScript able to infer the type in one case but not in the other?

@jack-williams
Copy link
Collaborator

I think this is probably a duplicate of #9366

TLDR; TypeScript doesn't do higher rank inference so in these examples if you don't give a type annotation explicitly TypeScript will infer {} for the types automatically.

@ivan7237d
Copy link
Author

ivan7237d commented Feb 20, 2018

@jack-williams Since #15680 was addressed, TypeScript would normally infer generic types successfully in the specific case of a function like pipe above, with arguments contextually typed left-to-right, and in this example it works for the second line.

[edited]

@jack-williams
Copy link
Collaborator

jack-williams commented Feb 20, 2018

EDIT: I'm not asserting this is what actually happens, but it's my best guess. I'd be interested to know too.

I think the reason why b works but a does not is because for b the generic (higher-rank) parameters of call are constrained by both the left and right inputs to pipe.

The input type A of call is constrained by the first input 1. The output type B of call is constrained by the third input, which is monomorphic of type number => number.

In a call appears last so the input type of call gets inferred to number by the second argument of type number => number, but there is nothing to constrain C, the return type of call. Before TypeScript seems to enter call to type-check the body under assumption B = number, it instantiates any remain parameters (C) to {}.

I think the reason your refactoring works is because c has type any => any, not number => number.

@ivan7237d
Copy link
Author

@jack-williams Thanks! This might be it, but yes, would be great to know for sure. I've tried to see if the inference would start to work if I add a type to a:

const a: number = pipe(1, x => x + 1, call(x => x + 1));

It didn't work, but I'm not at all sure this is a valid test of your suggestion.

@ivan7237d
Copy link
Author

I think the reason your refactoring works is because c has type any => any, not number => number.

Yes, that was my mistake, the part about refactoring was just a red herring and I deleted it.

@jack-williams
Copy link
Collaborator

jack-williams commented Feb 20, 2018

I think I was partly wrong; the type being unconstrained at the right hand side seems not to matter. For example this still gives an error:

const pipe = <A, B, C>(
  x: A,
  a: (x: A) => B, 
  b: (x: B) => C,
  c: (x: C) => number,
) => c(b(a(x)));

const call = <A>(f: (x: A) => A) => (x: A) => f(x)
const f = (x : number) => x + 1;
const a = pipe(1, x => x + 1, call(x => x + 1), f);

This makes me think that it just comes down to the fact it can infer the type for call when it's next to the literal 1, but when there is a higher-order value immediately on the left (such as x => x + 1), it defers inference and ends up making C = {} too early.

@ivan7237d
Copy link
Author

@jack-williams You know a funny thing? It infers types for

const c = pipe(1, call(x => x + 1), call(x => x + 1));

just fine (playground). This can't be right! It can infer types for the simpler program

a = pipe(1, x => x + 1, call(x => x + 1));

but not for the more complex one. In fact, by chance I've just run into an issue like this in my real code, and wrapping an arrow function in call(...) has fixed it, so call acts like a magic problem-solver. I think I'll close this issue and file a new one as a bug.

@microsoft microsoft locked and limited conversation to collaborators Jul 3, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants