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

Various issues when trying to use pipe function #29904

Closed
OliverJAsh opened this issue Feb 13, 2019 · 15 comments
Closed

Various issues when trying to use pipe function #29904

OliverJAsh opened this issue Feb 13, 2019 · 15 comments
Labels
Meta-Issue An issue about the team, or the direction of TypeScript

Comments

@OliverJAsh
Copy link
Contributor

OliverJAsh commented Feb 13, 2019

TypeScript Version: 3.3.1

Search Terms: pipe compose functional programming composition generics overloads

Code

It goes without saying that pipe is a very common utility function for composing functions, used often in functional programming. I've been using a pipe function in TypeScript for about 2 years, and over time I've collected numerous issues.

I wanted to create an issue to collect all of this information, to help others who want to use pipe so they are aware of the various footguns—and also for the TypeScript team to help their visibility of these issues.

To the best of my ability, I have narrowed these bugs down to the simplest examples, and provided any interesting (and sometimes useful) workarounds. I would appreciate any help in narrowing these examples further—perhaps even combining them where perceived issues are artefacts of the same underlying problems.

In my experience, 80% of the time pipe just works. These issues cover the remaining 20% of the time. Issue 1 is by far the most significant. The rest are unordered.

I have linked to sub issues where I'm aware they exist. For those without linked issues, we may want to create new issues to trick them independently.

1. Generics are lost when first composed function is generic

Related issues:

declare const pipe: {
    <A, B, C>(ab: (a: A) => B, bc: (b: B) => C): (a: A) => C;
};

type Component<P> = (props: P) => {};

declare const myHoc1: <P>(C: Component<P>) => Component<P>;
declare const myHoc2: <P>(C: Component<P>) => Component<P>;

declare const MyComponent1: Component<{ foo: 1 }>;

// When `strictFunctionTypes` disabled:
// Expected type: `(a: Component<{ foo: 1 }>) => Component<{ foo: 1 }>`
// Actual type: `(a: {}) => Component<{}>`
const enhance = pipe(
    /*
    When `strictFunctionTypes` enabled, unexpected type error::
    Argument of type '<P>(C: Component<P>) => Component<P>' is not assignable to parameter of type '(a: {}) => Component<{}>'.
        Types of parameters 'C' and 'a' are incompatible.
            Type '{}' is not assignable to type 'Component<{}>'.
                Type '{}' provides no match for the signature '(props: {}): {}'.
    */
    myHoc1,
    myHoc2,
);
// Expected type: `Component<{ foo: 1 }>`
// Actual type: `Component<{}>`
const MyComponent2 = enhance(MyComponent1);

// Workaround:
const enhance2 = pipe(
    () => myHoc1(MyComponent1),
    myHoc2,
);
const MyComponent3 = enhance2({});

With the option suggested in #27288, TypeScript would at least alert the developer to change the code to workaround this problem.

2. Incorrect overload is used for pipe

  • when strictFunctionTypes is disabled
  • when first composed function parameter is optional
  • when first pipe overload is zero parameters for first function

Related issues: #29913

declare const pipe: {
    // 0-argument first function
    // Workaround: disable this overload
    <A>(a: () => A): () => A;

    // 1-argument first function
    <A, B>(ab: (a: A) => B): (a: A) => B;
};

// Expected type: `(a: {} | undefined) => number`
// Actual type: `() => number`
const fn = pipe((_a?: {}) => 1);

3. Inference does not work when first pipe overload is zero parameters for first function

Related issues:

declare const pipe: {
    // 0-argument first function
    // Workaround: disable this overload
    <A, B>(a: () => A, ab: (a: A) => B): () => B;

    // 1-argument first function
    <A, B, C>(ab: (a: A) => B, bc: (b: B) => C): (a: A) => C;
};

// Example 1

type Fn = (n: number) => number;
const fn: Fn = pipe(
    // Expected param `x` type to be inferred as `number`
    // Actual type: any
    x => x + 1,
    x => x * 2,
);

// Example 2

const promise = Promise.resolve(1);
promise.then(
    pipe(
        // Expected param `x` type to be inferred as `number`
        // Actual type: any
        x => x + 1,
        x => x * 2,
    ),
);

4. Untitled

Related issues:

declare const pipe: {
    // Workaround 1: enable this overload
    // <A, B, C>(ab: (a: A) => B, bc: (b: B) => C): (a: A) => C;

    <A, B, C, D>(ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D): (a: A) => D;
};

declare const getString: () => string;
declare const orUndefined: (name: string) => string | undefined;
declare const identity: <T>(value: T) => T;

const fn = pipe(
    getString,

    /*
    Unexpected type error:
    Type 'string | undefined' is not assignable to type '{}'.
        Type 'undefined' is not assignable to type '{}'.
    */
    string => orUndefined(string),

    // Workaround 2: pass the function directly, instead of wrapping:
    // get,

    identity,
);

5. Incorrect overload is used for composed function

Related issues:

declare const pipe: {
    <A, B, C>(ab: (a: A) => B, bc: (b: B) => C): (a: A) => C;
};

declare const myFn: {
    (p: string): string;
    (p: any): number;
};

declare const getString: () => string;

// Expected type: `(a: {}) => string`
// Actual type: `(a: {}) => number`
// Note: if we comment out the last overload for `myFn`, we get the expected
// type.
const fn = pipe(
    getString,
    myFn,
);

6. Untitled

Related issues:

declare const pipe: {
    <A, B, C, D>(ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D): (a: A) => D;
};

declare const getArray: () => string[];
declare const first: <T>(ts: T[]) => T;

// When `strictFunctionTypes` disabled:
// Expected type: `(a: {}) => string`
// Actual type: `(a: {}) => {}`
const fn = pipe(
    getArray,
    x => x,
    /*
    When `strictFunctionTypes` enabled, unexpected type error:
    Argument of type '<T>(ts: T[]) => T' is not assignable to parameter of type '(c: {}) => {}'.
        Types of parameters 'ts' and 'c' are incompatible.
            Type '{}' is missing the following properties from type '{}[]': length, pop, push, concat, and 25 more.
    */
    first,
);

// Workaround 1: use `identity` function
declare const identity: <T>(value: T) => T;
const fn2 = pipe(
    getArray,
    identity,
    first,
);

// Workaround 2: wrap last function
const fn3 = pipe(
    getArray,
    x => x,
    x => first(x),
);
@ahejlsberg
Copy link
Member

@OliverJAsh Appreciate the write-up. See #30114 for an improvement that will allow you to use a generic rest parameter to represent the parameter list of the first function argument passed to pipe. This alleviates the need for overloads which in turn helps with some of the issues.

Other issues remain that we are aware of, such as support for inferring higher-order functions (the first issue) and issues relating to ordering of inferences when piping through combinations of contextually sensitive and contextually insensitive arguments (e.g. combinations of arrow functions and declared functions). We continue to contemplate possible solutions.

@RyanCavanaugh RyanCavanaugh added the Meta-Issue An issue about the team, or the direction of TypeScript label Feb 27, 2019
@ahejlsberg
Copy link
Member

With #30114 and #30193 all but issue 1 (inferring higher order function types) and issue 5 (overload resolution in inference and type relationships) above are now fixed. I'm definitely eager to address higher order function type inference (tracked by #9366) and I think we're getting closer to a workable solution.

@ahejlsberg
Copy link
Member

Higher order function type inference now implemented in #30215.

@weswigham
Copy link
Member

With all these PRs merged, can we call this closed?

@OliverJAsh
Copy link
Contributor Author

😮. Thank you so much, this is going to make a massive difference.

I'm not sure if all of the issues I listed above are fixed, so I'll defer to @ahejlsberg.

@ahejlsberg
Copy link
Member

Everything but issue 5 (overload resolution in type inference) has been fixed. That issue is already tracked elsewhere (e.g. #26591 and #29732) so I think we can safely call this closed.

@OliverJAsh
Copy link
Contributor Author

Presuming these fixes are now merged and released in next, I just tested all of the above examples with typescript@next.

1, 4, 6 are now working as expected. 🎉 (Thank you!)

2, 3, 5 however do not seem to have changed. IIUC, 5 is not expected to be fixed yet, but what about 2 and 3?

@ahejlsberg
Copy link
Member

The fix for 2 and 3 is to change your declarations of pipe to use generic rest parameters:

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;
// ...

These declarations will more accurately capture the parameter list of the first function and will work for any number of parameters in the first function.

@millsp
Copy link
Contributor

millsp commented Mar 26, 2019

Instead of writing overloads, we can write pipe & compose generically.

@OliverJAsh
Copy link
Contributor Author

OliverJAsh commented Apr 1, 2019

The fix for 2 and 3 is to change your declarations of pipe to use generic rest parameters:

@ahejlsberg Regarding issue 2 ("Incorrect overload is used for pipe"), is there any way to fix this without generic rest parameters, i.e. in some de-sugared form? I'm asking because I'd like to apply it to Lodash's pipe function, but it will be a long time before the Lodash typings support TS 3.4.

The first thing that came to mind was to switch the order of the overloads, but this breaks other examples:

declare const pipe: {
    <A, B>(a: () => A, ab: (a: A) => B): () => B;
    <A, B, C>(ab: (a: A) => B, bc: (b: B) => C): (a: A) => C;
};

// 1
{
    // Expected type: `(a: {} | undefined) => number`
    const fn = pipe(
        (_a?: {}) => 1,
        x => x,
    );
}

// 2
{
    declare const identity: <T>(t: T) => T;

    // Expected type: `<T>(t: T) => T`
    const fn2 = pipe(
        identity,
        value => identity(value),
    );
}

// 3
{
    declare const getString: () => string;

    // Expected type: `() => string`
    const fn1 = pipe(
        getString,
        x => x,
    );
}

Out of the examples above, 1 (with strictFunctionTypes disabled) and 2 are broken with the overload order shown. Switching the overloads fixes example 1 and 2 above, but breaks example 3.

declare const pipe: {
    <A, B, C>(ab: (a: A) => B, bc: (b: B) => C): (a: A) => C;
    <A, B>(a: () => A, ab: (a: A) => B): () => B;
};

Generic rest parameters fixes all examples, but as I mentioned, this isn't currently an option that's available to us.

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

@OliverJAsh
Copy link
Contributor Author

I think I've ran into another bug (using 3.4.1). Posted here: #30727.

@OliverJAsh
Copy link
Contributor Author

Plug: I created a small library just for pipe, which includes suggestions from this issue

https://github.com/unsplash/pipe-ts

@OliverJAsh
Copy link
Contributor Author

@ahejlsberg I have two new pipe bugs:

  1. pipe loses generics #30727
  2.  declare function pipe<A, B, C>(
         ab: (a: A) => B,
         bc: (b: B) => C,
     ): (a: A) => C;
     declare function pipe<A, B, C, D>(
         ab: (a: A) => B,
         bc: (b: B) => C,
         cd: (c: C) => D,
     ): (a: A) => D;
    
     declare const stringIdentityGeneric: <K extends string>(a: K) => K;
     declare const stringIdentity: (str: string) => string;
    
     pipe(
         // Unexpected error
         stringIdentityGeneric,
    
         // Workaround: enable this
         // x => x,
    
         stringIdentity,
     );

@NickHeiner
Copy link

NickHeiner commented Jan 29, 2021

@OliverJAsh's pipe-ts works great when the functions are applied from left to right. However, when I rewrite the typings to pipe functions from right to left, I get unknowns in the type result. Is this known / intended?

declare function pipe<A, B, C>(
    ab: (this: void, a: A) => B,
    bc: (this: void, b: B) => C,
): (arg: A) => C;

declare function pipeReverse<A, B, C>(
    bc: (this: void, b: B) => C,
    ab: (this: void, a: A) => B,
): (arg: A) => C;

type Box<T> = {val: T};
type Bag1 = {a: boolean; b: string};
type Bag2 = {c: number; d: object};
type Bag3 = {e: 'e val'; f: 'f val'};

declare function withBag2<T>(b: Box<T>): Box<T & Bag2>;
declare function withBag3<T>(b: Box<T>): Box<T & Bag3>;

const startingValue: Box<{starting: 'value'}> = {val: {starting: 'value'}};

// Piping from left to right works.
const baggedViaPipe = pipe(withBag2, withBag3)(startingValue);

// Piping from right to left does not.
//
// Argument of type '<T>(b: Box<T>) => Box<T & Bag3>' is not assignable to parameter of type '(this: void, a: unknown) => Box<T>'.
//  Types of parameters 'b' and 'a' are incompatible.
//    Type 'unknown' is not assignable to type 'Box<T>'.(2345)
const baggedViaPipeRev = pipeReverse(withBag2, withBag3)(startingValue);

TS Playground

@weswigham
Copy link
Member

The specific sub-issues people have encountered which remain have their own sub-issues at this point - I'm going to close this issue in favor of them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Meta-Issue An issue about the team, or the direction of TypeScript
Projects
None yet
Development

No branches or pull requests

6 participants