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

pipe loses generics #30727

Open
OliverJAsh opened this issue Apr 3, 2019 · 8 comments
Open

pipe loses generics #30727

OliverJAsh opened this issue Apr 3, 2019 · 8 comments
Assignees
Labels
Needs Investigation This issue needs a team member to investigate its status.

Comments

@OliverJAsh
Copy link
Contributor

TypeScript Version: 3.4.1

Search Terms: generic rest parameters pipe compose

Code

I'm defining pipe using generic rest parameters as recommended here: #29904 (comment).

// Copied from https://github.com/Microsoft/TypeScript/issues/29904#issuecomment-471334674
declare function pipe<A extends any[], B>(ab: (...args: A) => B): (...args: A) => B;
declare function pipe<A extends any[], B, C>(
    ab: (...args: A) => B,
    bc: (b: B) => C,
): (...args: A) => C;
declare function pipe<A extends any[], B, C, D>(
    ab: (...args: A) => B,
    bc: (b: B) => C,
    cd: (c: C) => D,
): (...args: A) => D;

declare const myGenericFn: <T>(t: T) => string[];
declare const join: (strings: string[]) => string;

// Expected type: `<T>(t: T) => string`
// Actual type: `(t: any) => string`
const fn1 = pipe(
    myGenericFn,
    join,
);

// Workaround:
// Expected and actual type: `<T>(t: T) => string`
const fn2 = pipe(
    myGenericFn,
    strings => join(strings),
);

Playground Link: https://www.typescriptlang.org/play/index.html#src=declare%20function%20pipe%3CA%20extends%20any%5B%5D%2C%20B%3E(ab%3A%20(...args%3A%20A)%20%3D%3E%20B)%3A%20(...args%3A%20A)%20%3D%3E%20B%3B%0D%0Adeclare%20function%20pipe%3CA%20extends%20any%5B%5D%2C%20B%2C%20C%3E(%0D%0A%20%20%20%20ab%3A%20(...args%3A%20A)%20%3D%3E%20B%2C%0D%0A%20%20%20%20bc%3A%20(b%3A%20B)%20%3D%3E%20C%2C%0D%0A)%3A%20(...args%3A%20A)%20%3D%3E%20C%3B%0D%0Adeclare%20function%20pipe%3CA%20extends%20any%5B%5D%2C%20B%2C%20C%2C%20D%3E(%0D%0A%20%20%20%20ab%3A%20(...args%3A%20A)%20%3D%3E%20B%2C%0D%0A%20%20%20%20bc%3A%20(b%3A%20B)%20%3D%3E%20C%2C%0D%0A%20%20%20%20cd%3A%20(c%3A%20C)%20%3D%3E%20D%2C%0D%0A)%3A%20(...args%3A%20A)%20%3D%3E%20D%3B%0D%0A%0D%0Adeclare%20const%20myGenericFn%3A%20%3CT%3E(t%3A%20T)%20%3D%3E%20string%5B%5D%3B%0D%0Adeclare%20const%20join%3A%20(strings%3A%20string%5B%5D)%20%3D%3E%20string%3B%0D%0A%0D%0A%2F%2F%20Expected%20type%3A%20%60%3CT%3E(t%3A%20T)%20%3D%3E%20string%60%0D%0A%2F%2F%20Actual%20type%3A%20%60(t%3A%20any)%20%3D%3E%20string%60%0D%0Aconst%20fn1%20%3D%20pipe(%0D%0A%20%20%20%20myGenericFn%2C%0D%0A%20%20%20%20join%2C%0D%0A)%3B%0D%0A%0D%0A%2F%2F%20Workaround%3A%0D%0A%2F%2F%20Expected%20and%20actual%20type%3A%20%60%3CT%3E(t%3A%20T)%20%3D%3E%20string%60%0D%0Aconst%20fn2%20%3D%20pipe(%0D%0A%20%20%20%20myGenericFn%2C%0D%0A%20%20%20%20strings%20%3D%3E%20join(strings)%2C%0D%0A)%3B%0D%0A

Related Issues: #29904

/cc @ahejlsberg

@weswigham
Copy link
Member

Looks like the type is being pulled from the constraint on pipe's type parameter - use unknown[] instead of any[] and you get unknown as the parameter type instead.

@OliverJAsh
Copy link
Contributor Author

Is that case, my next question is why does it not propagate the generic?

@OliverJAsh OliverJAsh changed the title pipe generic falls back to any pipe loses generics Apr 3, 2019
@ahejlsberg
Copy link
Member

Here's how resolution of the call proceeds:

  • Processing of the first argument (myGenericFn) is deferred because it is a generic function.
  • Processing of the second argument produces inference candidates for B and C.
  • Processing of the first argument notices that inference candidates exist for A or B. Therefore, T isn't promoted, but rather myGenericFn is instantiated using the existing inferences--and since there are no inference candidates for A you get the default constraint any.

It's not immediately clear how we can do better here.

@weswigham
Copy link
Member

but rather myGenericFn is instantiated using the existing inferences--and since there are no inference candidates for A you get the default constraint any.

What if we only produce inferences for the inferred part of the context, akin to the cloneInferredPartOfContext call we do for return type inference? (And then promote the parameters we don't have inferences for)

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Apr 9, 2019
@babakness
Copy link

Hey guys, for what its worth, I've come up with a recursive Pipe and Compose which preserves parameter names. I think this is a better experience than have variables names like a, b etc.

It tolerates generics better but this too has issues with generics.

Anyway, I just published this library

Type only:

https://github.com/babakness/pipe-and-compose-types

Implementation:

https://github.com/babakness/pipe-and-compose

and here is an article I wrote on it

https://dev.to/babak/introducing-the-recursive-pipe-and-compose-types-3g9o

@babakness
Copy link

Update.

I believe the key issue with generics in the recursive version has to do with infer. These are the two main helpers that Pipe from

https://github.com/babakness/pipe-and-compose-types

Relies on

/**
 * Extracts function arguments
 */
export type ExtractFunctionArguments < Fn > = Fn extends  ( ...args: infer P ) => any  ? P : never

/**
 * Extracts function return values
 */
export type ExtractFunctionReturnValue<Fn> = Fn extends  ( ...args: any[] ) => infer P  ? P : never

Example

type Foo = ExtractFunctionArguments< <A>(a:A, b:number) => A >
// Foo has type [ {} , number ]

@jcalz
Copy link
Contributor

jcalz commented Nov 12, 2019

In researching this SO question I landed here, and I'm not sure if this is the same issue or a different one:

declare function pipe<A extends any[], B, C>(
  ab: (...args: A) => B,
  bc: (b: B) => C,
): (...args: A) => C;

declare function list<T>(a: T): T[];
declare function acceptNumArray(x: number[]): void

const f = pipe(list, acceptNumArray);
// inferred as pipe<[any], number[], void>(list, acceptNumArray)
// why is A inferred as [any] and not [number]?

In the above, B and C are correctly inferred, but A is not. Does the reasoning in this comment apply here too? It seems reasonable that:

  • Processing of the first argument (list) is deferred because it is a generic function.

  • Processing of the second argument produces inference candidates for B and C.

  • Processing of the first argument notices that inference candidates exist for A or B. Therefore, T isn't promoted, but rather list is instantiated using the existing inferences

But then the rest of it, "and since there are no inference candidates for A you get the default constraint any" doesn't sound right. It seems that if list is instantiated so that its return type has to match the inferred B type, then there's an inference candidate for A. Why does this one fail?

Thanks!

@ahejlsberg
Copy link
Member

Does the reasoning in this comment apply here too?

Yes, that reasoning applies to this example as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Investigation This issue needs a team member to investigate its status.
Projects
None yet
Development

No branches or pull requests

6 participants