-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Pipe/flow/chain type support #30370
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
Comments
As this needs a proposal, here’s my $0.02, using the example of a hypothetical observable library heavily inspired by RxJS.
In a regular parameter, In a rest parameter, In the return type, Note: I can’t think of a way to reference these types in the method body. It might be impossible. For that reason, my proposal is limited to method/function declaration only. It wouldn’t be value syntax for method/function implementations. The implementation of functions and methods would be no worse than the current situation, but at least usages of these functions and methods would now be type checked to any depth without having to pump out a dozen overloads. |
I suppose if were spitballing, Ill take a stab as well. I do think thats a decent idea, but its a little hairy because the generic param type Operation<In,Out> = (x: In) => Out
pipe2<T, U, V, X, Y, ...A extends [Operation<T,U>, Operation<U,V>, ...A], (...ops: A[], opLast<X,Y>): Y {
} but even this doesnt really describe that the 2nd and 3rd arguments should follow the same pattern as the 1st and 2nd. |
My vote would be to describe such types using something similar to the mathematical notation where
|
I managed to implement a pipe function type. Instead of overloading, it uses tuple rest args and recursion. There are 2 flavors of the pipe type. One returns a function ( // A is argument type, R is return type
type Fn<A = any, R = any> = (a: A) => R;
type PipeResult<A, T extends Fn[]> = T extends [Fn<any, infer R>, ...infer Rest]
? Rest extends [Fn, ...any]
? PipeResult<A, Rest>
: Fn<A, R>
: never;
type PipeArgs<A, T extends Fn[]> = T extends [Fn<any, infer R>, ...infer Rest]
? Rest extends [Fn, ...any]
? [Fn<A, R>, ...PipeArgs<R, Rest>]
: [Fn<A, R>]
: T;
// usage: pipe(fn0, fn1, fn2...)(x)
type Pipe = <A, T extends Fn[]>(...fns: PipeArgs<A, T>) => PipeResult<A, T>;
// usage: pipe(x, fn0, fn1, fn2...)
type PipeValue = <A, T extends Fn[]>(a: A, ...fns: PipeArgs<A, T>) => ReturnType<PipeResult<A, T>>;
// example implementation (I apologize for my liberal use of any)
const pipe: Pipe = ((...fns: any[]) => (x: any) => fns.reduce((v, f) => f(v), x)) as any;
const pipeValue: PipeValue = ((x: any, ...fns: any[]) => fns.reduce((v, f) => f(v), x)) as any; It correctly identifies the first incorrect argument. For compose, just switch around the inferring of A and R type ComposeResult<R, T extends Fn[]> = T extends [Fn<infer A, any>, ...infer Rest]
? Rest extends [Fn, ...any]
? ComposeResult<R, Rest>
: Fn<A, R>
: never;
type ComposeArgs<R, T extends Fn[]> = T extends [Fn<infer A, any>, ...infer Rest]
? Rest extends [Fn, ...any]
? [Fn<A, R>, ...ComposeArgs<A, Rest>]
: [Fn<A, R>]
: T;
type Compose = <R, T extends Fn[]>(...fns: ComposeArgs<R, T>) => ComposeResult<R, T>;
const compose: Compose = ((...fns: any[]) => (x: any) => fns.reduceRight((v, f) => f(v), x)) as any; |
Works great @profound7. I wonder, is it also possible to optionally infer the value of the first function from the composed function argument? // Works great, 2 is checked to be a number
const x = pipe((a: number) => a.toString(), x => +x)(2)
// Could we infer `a` in the first function to be the type inferred from 2 (Number | '2')?
const y = pipe(a => a.toString(), x => +x)(2) |
@profound7, unfortunately this still performs worse that overloads implementations, since it can not infer types from a previous function in a chain: pipe(
(n: number) => n * 2,
(n) => {}, // n should be `number`
// ^? (parameter) n: any
); This looks to be a Typescript bug, since the playground even suggests that Is there any progress to fully support chain types in Typescript? The issue is here since 2019... |
This is the best (shortest & fullest) solution I've made so far:
type Fn<A = any[], Z = any> = (...args: A extends any[] ? A : [A]) => Z;
type Arg1<A, Z> = [Fn<A, Z>];
type Arg2<A, B, Z> = [Fn<A, B>, Fn<[B], Z>];
type Arg3<A, B, C, Z> = [...Arg2<A, B, C>, Fn<[C], Z>];
type Arg4<A, B, C, D, Z> = [...Arg3<A, B, C, D>, Fn<[D], Z>];
type Arg5<A, B, C, D, E, Z> = [...Arg4<A, B, C, D, E>, Fn<[E], Z>];
type Arg6<A, B, C, D, E, F, Z> = [...Arg5<A, B, C, D, E, F>, Fn<[F], Z>];
type Arg7<A, B, C, D, E, F, G, Z> = [...Arg6<A, B, C, D, E, F, G>, Fn<[G], Z>];
type ArgAny<A, B, C, D, E, F, G, H, Z extends Fn[]> = [
...Arg7<A, B, C, D, E, F, G, H>,
...Args<H, Z>
];
type Args<A, Z extends Fn[]> = Z extends [Fn<any, infer R>, ...infer Rest]
? Rest extends [Fn, ...any]
? [Fn<A, R>, ...Args<R, Rest>]
: [Fn<A, R>]
: Z;
type Result<A, Z extends Fn[]> = Z extends [Fn<any, infer R>, ...infer Rest]
? Rest extends [Fn, ...any]
? Result<A, Rest>
: Fn<A, R>
: never;
type Pipe = {
<A>(): Fn<A, A extends any[] ? A[0] : A>;
<A, Z>(...fn: Arg1<A, Z>): Fn<A, Z>;
<A, B, Z>(...fn: Arg2<A, B, Z>): Fn<A, Z>;
<A, B, C, Z>(...fn: Arg3<A, B, C, Z>): Fn<A, Z>;
<A, B, C, D, Z>(...fn: Arg4<A, B, C, D, Z>): Fn<A, Z>;
<A, B, C, D, E, Z>(...fn: Arg5<A, B, C, D, E, Z>): Fn<A, Z>;
<A, B, C, D, E, F, Z>(...fn: Arg6<A, B, C, D, E, F, Z>): Fn<A, Z>;
<A, B, C, D, E, F, G, Z>(...fn: Arg7<A, B, C, D, E, F, G, Z>): Fn<A, Z>;
<A, B, C, D, E, F, G, H, Z extends Fn[]>(
...fn: ArgAny<A, B, C, D, E, F, G, H, Z>
): Result<A, Z>;
}; If you can think of a better/shorter solution, please post it in this thread and tag me. Thanks. UPDATE: an improved version |
@Azarattum great, thanks for sharing! It's me or |
@Bessonov this is by design. I did this intentionally to demonstrate that type inference works only for the first 7 functions. Since they are implemented manually with overloads. The rest is just type checked but not inferred. So, if you write |
I've made an updated version which:
Playground
|
@profound7 |
I made this work with async its not clean but works well: type IsPromise<T> = T extends Promise<any> ? true : false;
type SomePromise<T extends any[]> = T extends [infer First, ...infer Rest]
? IsPromise<First> extends true
? true
: SomePromise<Rest>
: false;
type Fn<A extends unknown[], R> = (...args: A) => R;
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;
type Result<R extends unknown[]> = SomePromise<R> extends true
? (...args: [R[0]]) => Promise<Last<Awaited<R>>>
: (...args: [R[0]]) => Last<Awaited<R>>;
type ChainOverload = {
<A extends unknown[], R0>(fn0: Fn<A, R0>): (...args: A) => Result<[R0]>;
<A extends unknown[], R0, R1>(fn0: Fn<A, R1>, fn1: Fn<[Awaited<R0>], R1>): Result<
[R0, R1]
>;
<A extends unknown[], R0, R1, R2>(
fn0: Fn<A, R0>,
fn1: Fn<[Awaited<R0>], R1>,
fn2: Fn<[Awaited<R1>], R2>
): Result<[R0, R1, R2]>;
<A extends unknown[], R0, R1, R2, R3>(
fn0: Fn<A, R0>,
fn1: Fn<[Awaited<R0>], R1>,
fn2: Fn<[Awaited<R1>], R2>,
fn3: Fn<[Awaited<R2>], R3>
): Result<[R0, R1, R2, R3]>;
<A extends unknown[], R0, R1, R2, R3, R4>(
fn0: Fn<A, R0>,
fn1: Fn<[Awaited<R0>], R1>,
fn2: Fn<[Awaited<R1>], R2>,
fn3: Fn<[Awaited<R2>], R3>,
fn4: Fn<[Awaited<R3>], R4>
): Result<[R0, R1, R2, R3, R4]>;
<A extends unknown[], R0, R1, R2, R3, R4, R5>(
fn0: Fn<A, R0>,
fn1: Fn<[Awaited<R0>], R1>,
fn2: Fn<[Awaited<R1>], R2>,
fn3: Fn<[Awaited<R2>], R3>,
fn4: Fn<[Awaited<R3>], R4>,
fn5: Fn<[Awaited<R4>], R5>
): Result<[R0, R1, R2, R3, R5, R5]>;
// todo: add more here ...
};
const chain: ChainOverload = (() => {}) as any;
const final = chain(
(one: string) => one,
async (two) => Number(two),
(three) => three
);
export const result: number = await final('hey'); |
Also I guess: import { Is } from '@janekeilts/is';
import { Pipe } from '@Pipe';
type SingleArgFunction = (arg: any) => any;
export const pipe: Pipe = (...fns: SingleArgFunction[]) => {
const somePromise: boolean = fns.some(Is.Function.async);
return somePromise
? async (arg: any) => {
let result = arg;
for (const fn of fns) result = await fn(result);
return result;
}
: (arg: any) => {
let result = arg;
for (const fn of fns) result = fn(arg);
return result;
};
}; |
Search Terms
recursive, flow, pipe, chain
Suggestion
I do not have a specific implementation in mind to support this. There is a decent existing solution by a stackoverflow user @jcalz, which does its best given what typescript is able to accomplish now. I have created this issue to more to specifically point out the current limitation of typescript in regard to this feature, and the current use cases. This feature request falls under the larger umbrella of recursive type inference.
At its most broad, I would like to suggest that typescript include some form of an official
Chain
type that developers can use.Unanswered Questions
pipe: Chain<In, Out, number>
Use Cases
There are a number of implementations for javascript pipes in common libraries.
Related Issues
#27102
#28505
Examples
a typical implementation looks like this:
The limitation of the current implementation is that if you use a typed pipe function with more arguments than the written overloads, you run lose type inference.
Checklist
My suggestion meets these guidelines:
The text was updated successfully, but these errors were encountered: