Typescript: `pipe` loses generics

Created on 3 Apr 2019  路  8Comments  路  Source: microsoft/TypeScript


TypeScript Version: 3.4.1


Search Terms: generic rest parameters pipe compose

Code

I'm defining pipe using generic rest parameters as recommended here: https://github.com/Microsoft/TypeScript/issues/29904#issuecomment-471334674.

// 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: https://github.com/Microsoft/TypeScript/issues/29904

/cc @ahejlsberg

Needs Investigation

Most helpful comment

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

All 8 comments

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.

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

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.

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)

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

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 ]

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!

Does the reasoning in this comment apply here too?

Yes, that reasoning applies to this example as well.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

wmaurer picture wmaurer  路  3Comments

bgrieder picture bgrieder  路  3Comments

remojansen picture remojansen  路  3Comments

manekinekko picture manekinekko  路  3Comments

jbondc picture jbondc  路  3Comments