Description
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:
- Generic not inferred from contextual types #25293
- Problem explanation: Generic not inferred from contextual types #25293 (comment)
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:
- Generic inference inside of
pipe
is incorrect when strict mode is disabled #25826 - Problem explanation: Generic inference inside of
pipe
is incorrect when strict mode is disabled #25826 (comment)
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:
- Incorrect overload is used for composed function inside
pipe
#25637 - Incorrect overload is used for composed function inside
pipe
#25637 (comment)
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:
- Unexpected error/bad inference for generic composed function inside
pipe
#25791 - Unexpected error/bad inference for generic composed function inside
pipe
#25791 (comment)
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),
);