How to define a simple pipe function with generics? - javascript

I wrote a simple pipe function that accepts either asynchronous functions, or just values that are passed on without being executed.
I really tried to define it using generics but didn't make it so reverted to using unknown instead.
What I have:
export const pipe = (...args: Array<unknown>): Promise<unknown> | unknown =>
args.reduce((prev, exec) => {
if (typeof exec !== 'function') {
return exec;
}
const getNextInPipe = async (): Promise<unknown> => {
return exec(await prev);
};
const value = getNextInPipe();
return value;
});
I tried to write it like this:
export const pipe = <T,>(...args: Array<unknown>): unknown =>
args.reduce((prev, exec) => {
if (typeof exec !== 'function') {
return exec;
}
const getNextInPipe = async (): Promise<T> => {
return exec(await prev);
};
const value = getNextInPipe();
return value;
});
But I don't know how to replace the other unknown, and if it can be done? Because the type of output of each function in the pipe doesn't depend on the type of input.
I'm still new to generics, thanks in advance

Your function might be simple (that's debatable anyway) but the generic typings are anything but. You are trying to represent a "chain" of types of arbitrary length. Essentially you start with an initial value of type I, and then maybe a function of a type like (input: Awaited<I>) => Promise<Awaited<TFirst>> for some output type TFirst, and then maybe a function of a type like (input: Awaited<TFirst>) => Promise<Awaited<TSecond>>, etc. etc., and finally ending on a function of a type like (input: Awaited<TPenultimate>) => Promise<Awaited<TLast>>, and then the output of pipe() is a value of type Promise<Awaited<TLast>>, unless there were no functions and just an input I, in which case the output is I.
The parts with the Awaited type are dealing with the fact that if you await a non-promise value you get the value, so Awaited<string> is string, and Awaited<Promise<string>> is string... and you can't really nest promises, so Awaited<Promise<Promise<string>>> is also string.
So one approach to pipe() would look like this:
const pipe: <I, T extends any[]>(
init: I,
...fns: { [N in keyof T]: (input: Awaited<Idx<[I, ...T], N>>) => T[N] }
) => T extends [...infer _, infer R] ? Promise<Awaited<R>> : I =
(...args: any[]): any => args.reduce((prev, exec) => {
if (typeof exec !== 'function') {
return exec;
}
const getNextInPipe = async () => {
return exec(await prev);
};
const value = getNextInPipe();
return value;
});
type Idx<T, K> = K extends keyof T ? T[K] : never;
The I type parameter corresponds to the type of the init function parameter. The T type parameter corresponds to the tuple of the output types of each of the functions in the fns rest parameter. So if there are two functions and the first function returns a Promise<boolean> and the second function returns a string, then T will be [Promise<boolean>, string].
The type of the fns argument is where the complexity lives. For the element of fns at numericlike index N (think 0 for the first one, 1 for the second one), we know that the output type is the Nth element of T, or the indexed access type T[N]. That's straightforward enough. But the input type comes from the previous element of T. Or maybe I. We represent that by first making [I, ...T], which uses a variadic tuple type to represent prepending I to T. Then we just need the Nth element of that. Conceptually that's the indexed access [I, ...T][N]. But the compiler isn't smart enough to realize that every numeric index N of the T tuple type will also be an index on the [I, ...T] tuple type. So I need to use the Idx helper type to convince the compiler to perform that indexing.
As for the output type, we need to tease apart T to find its last element R (using conditional type inference). If that exists, then we are returning a value of type Promise<Awaited<R>>. If not, it's because T is empty so we're just returning I.
Whew.
Okay let's test it. First of all the supported uses:
const z = pipe(3, (n: number) => n.toFixed(2), (s: string) => s.length === 4)
// const pipe: <3, [string, boolean]>(
// init: 3,
// fns_0: (input: 3) => string,
// fns_1: (input: string) => boolean
// ) => Promise<boolean>
// const z: Promise<boolean>
z.then(v => console.log("z is", v)) // z is true
const y = pipe(4);
// const pipe: <4, []>(init: 4) => 4
// const y: 4
console.log("y is", y) // y is 4
const x = pipe(50, (n: number) => new Promise<string>(
r => setTimeout(() => { r(n.toFixed(3)) }, 1000)),
(s: string) => s.length === 4);
// const pipe: <50, [Promise<string>, boolean]>(
// init: 50,
// fns_0: (input: 50) => Promise<string>,
// fns_1: (input: string) => boolean
// ) => Promise<boolean>
// const x: Promise<boolean>
x.then(v => console.log("x is", v)) // x is false
That all looks good. z and x are promises of the expected type, while y is just a numeric value. Now for the unsupported cases:
pipe(); // error!
// Expected at least 1 arguments, but got 0.
pipe(10, 20, 30); // error!
// Argument of type 'number' is not assignable to parameter of type '(input: 10) => unknown'.
pipe(10, (x: string) => x.toUpperCase()) // error!
// Type 'number' is not assignable to type 'string'.
pipe(10, (x: number) => x.toFixed(2), (x: boolean) => x ? "y" : "n") // error!
// Type 'string' is not assignable to type 'boolean'
Those all fail for violating constraints on the function. It needs at least one argument, and only the first argument can be a non-function. Each function needs to accept the awaited response of the previous function (or the initial value), and if it does not, then you get an error.
So that's about as good a job as I can do. It's not perfect; I'm sure you could find edge cases if you look. The obvious one is if you don't annotate the callback parameters then inference might fail. Something like pipe(10, x => x.toFixed(), y => y.toFixed()) should yield an error but doesn't, because the compiler fails to infer that x should be a number and it falls back to any, after which all the inputs and outputs are any. If you want it to be caught you need to write pipe(10, (x: number)=>x.toFixed(), (y: number)=>y.toFixed()). There may be tweaks that can improve this, but I'm not going to spend any more time trying to find them here.
The main point is that you can represent this sort of thing but it's not simple.
Playground link to code

Related

TypeScript `unknown` doesn't allow non-unknown types in function parameters

Why doesn't this work?
const x: unknown[] = ['x', 32, true]; // OK
const y: (...args: unknown[]) => unknown = (xx: number) => {}; // ERROR
// Type '(xx: number) => void' is not assignable to type '(...args: unknown[]) => unknown'.
// Types of parameters 'xx' and 'args' are incompatible.
// Type 'unknown' is not assignable to type 'number'. ts(2322)
My goal is to make sure that y is any runnable function. I was trying not to use any.
Hope to improve my understanding of how unknown works in this case.
Function types are contravariant in their parameter types; see Difference between Variance, Covariance, Contravariance and Bivariance in TypeScript for more details. Contravariance means the direction of assignability flips; if T is assignable to U, then (...u: U) => void is assignable to (...t: T) => void and not vice versa. This is necessary for type safety. Picture the direction of data flow: if you want fruit then I can give you an apple, but if you want something that will eat all your fruit I can't give you something that eats only apples.
The function type (xx: number) => void is equivalent to (...args: [number]) => void, and you cannot assign that to (...args: unknown[]) => void. Yes, [number] is assignable to unknown[], but that's not the direction we care about. Your assignment is therefore unsafe. If this worked:
const y: (...args: unknown[]) => unknown =
(xx: number) => xx.toFixed(); // should this be allowed?
Then you'd be able to call y() with any set of arguments you wanted without a compiler error, but hit a runtime error:
y("x", 32, true); // no compiler error
// 💥 error! xx.toFixed is not a function
Widening the input argument list to unknown[] has the effect of making the function type very narrow, since most functions do not accept all possible argument lists.
So if you really want a type to which any function at all should be assigned, you'd need to narrow the input argument list to a type that cannot accept any inputs, like this:
type SomeFunction = (...args: never) => unknown;
const y: SomeFunction = (xx: number) => xx.toFixed(); // okay
// const y: SomeFunction
That works because SomeFunction is essentially uncallable (well, it should be; there's an outstanding bug at ms/TS#48840). If I ask you for a function that I'm not going to call, you can safely hand me any function at all. Conversely if you hand me a function and I don't know what arguments it accepts, I had better not try to call it.
So that works, but... it's kind of useless. Once you have y you can't do anything with it:
y(123); // error, oops doesn't know about its argument types anymore
For the use case in your question I guess that's fine, since you are passing these uncallable functions to non-TypeScript code to something which has no knowledge of or regard for our type safety rules.
Still, for others who might be reading, it might be more useful to verify that a value can be assigned to a type without widening it to that type. So instead of
annotating y as SomeFunction, we can use the satisfies operator to just check it against that type:
const y = ((xx: number) => xx.toFixed()) satisfies SomeFunction;
// const y: (xx: number) => string
That compiles (but would fail if you wrote, say, const y = "oops" satisifies SomeFunction), and y is still known to be (xx: number) => string. So you still can call it:
y(123); // okay
Playground link to code
Why doesn't this work?
The following is an error:
const x: unknown[] = ['x', 32, true]; // OK
const y: (...args: unknown[]) => unknown = (xx: number) => {}; // ERROR
Because of the same reason why this is an error:
const x: unknown = 123; // OK
const y: number = x; // ERROR: cannot assign unknown to number
Reason Simplified: You cannot assign unknown to anything else without first checking its runtime value.

Typescript type for an array of functions that each transform the data [duplicate]

I want to create a function chain, which would be an input of a pipe/flow/compose function.
Is this possible without the literal expansion of the types to selected depth, as is this usually handled? See lodash's flow.
I want to achieve typecheck of the data flow in the chain.
- Argument of a function is result of the previous one
- First argument is a template parameter
- Last return is a template parameter
type Chain<In, Out, Tmp1 = any, Tmp2 = any> = [] | [(arg: In) => Out] | [(arg: In) => Tmp1, (i: Tmp1) => Tmp2, ...Chain<Tmp2, Out>];
The idea is in the draft.
This however produces tho following errors:
Type alias 'Chain' circularly references itself. (understand why, don't know how to resole)
A rest element type must be an array type. (probably spread is not available for generic tuples)
Type 'Chain' is not generic. (don't even understand why this error is even here)
Is this definition of Chain possible in Typescript? If so, please enclose a snippet.
(Tested on latest tsc 3.1.6)
Circular type aliases are not really supported except in certain cases. (UPDATE TS 4.1, these are more supported now, but I'm still inclined to represent flow() as operating on AsChain that verifies a particular array of functions instead of trying to come up with a Chain that matches all valid arrays of functions)
Instead of trying to represent the specific type you've written there in a TypeScript-friendly way, I think I'll back up and interpret your question as: how can we type a flow()-like function, which takes as its arguments a variable number of one-argument functions, where each one-argument-function return type is the argument type for the next one-argument-function, like a chain... and which returns a one-argument function representing the collapsed chain?
I've got something that I believe works, but it's quite complicated, using a lot of conditional types, tuple spreads, and mapped tuples. Here it is:
type Lookup<T, K extends keyof any, Else=never> = K extends keyof T ? T[K] : Else
type Tail<T extends any[]> = T extends [any, ...infer R] ? R : never;
type Func1 = (arg: any) => any;
type ArgType<F, Else=never> = F extends (arg: infer A) => any ? A : Else;
type AsChain<F extends [Func1, ...Func1[]], G extends Func1[]= Tail<F>> =
{ [K in keyof F]: (arg: ArgType<F[K]>) => ArgType<Lookup<G, K, any>, any> };
type Last<T extends any[]> = T extends [...infer F, infer L] ? L : never;
type LaxReturnType<F> = F extends (...args: any) => infer R ? R : never;
declare function flow<F extends [(arg: any) => any, ...Array<(arg: any) => any>]>(
...f: F & AsChain<F>
): (arg: ArgType<F[0]>) => LaxReturnType<Last<F>>;
Let's see if it works:
const stringToString = flow(
(x: string) => x.length,
(y: number) => y + "!"
); // okay
const str = stringToString("hey"); // it's a string
const tooFewParams = flow(); // error
const badChain = flow(
(x: number)=>"string",
(y: string)=>false,
(z: number)=>"oops"
); // error, boolean not assignable to number
Looks good to me.
I'm not sure if it's worth it to go through in painstaking detail about how the type definitions work, but I might as well explain how to use them:
Lookup<T, K, Else> tries to return T[K] if it can, otherwise it returns Else. So Lookup<{a: string}, "a", number> is string, and Lookup<{a: string}, "b", number> is number.
Tail<T> takes a tuple type T and returns a tuple with the first element removed. So Tail<["a","b","c"]> is ["b","c"].
Func1 is just the type of a one-argument function.
ArgType<F, Else> returns the argument type of F if it's a one-argument function, and Else otherwise. So ArgType<(x: string)=>number, boolean> is string, and ArgType<123, boolean> is boolean.
AsChain<F> takes a tuple of one-argument functions and tries to turn it into a chain, by replacing the return type of each function in F with the argument type of the next function (and using any for the last one). If AsChain<F> is compatible with F, everything's good. If AsChain<F> is incompatible with F, then F is not a good chain. So, AsChain<[(x: string)=>number, (y:number)=>boolean]> is [(x: string)=>number, (y: number)=>any], which is good. But AsChain<[(x: string)=>number, (y: string)=>boolean]> is [(x: string)=>string, (y: string)=>any], which is not good.
Last<T> takes a tuple and returns the last element, which we need to represent the return type of flow(). Last<["a","b","c"]> is "c".
Finally, LaxReturnType<F> is just like ReturnType<F> but without a constraint on F.
Okay, hope that helps; good luck!
Playground link to code

Is there any way to get a names of arguments of function in typescript? [duplicate]

This question already has answers here:
Get function parameter names and types in TypeScript
(1 answer)
How to get function parameter names/values dynamically?
(34 answers)
Closed 1 year ago.
I am trying to make a wrapper function which will take a function as an input and will return a newly typed function which allow both a list of parameters and an object which contain parameter names as keys.
I have written the following code for that. The code works as expected. The problem is that I have pass an additional type which contains the keys as parameter name and value as type of that parameter. I want to do this dynamically. I want to access the names of the parameters
//I want remove this Args parameters and make it dynamically using F type.
function wrapper<F extends (...args: any) => any, Args>(func: unknown) {
type ParametersList = Parameters<F>;
return func as (...args: [Args] | ParametersList) => ReturnType<F>;
}
const add = (x: number, y: number) => x + y;
const wrappedAdd = wrapper<typeof add, { x: number; y: number }>(add);
The Parameters function returns a named tuple(which is a new feature I guess). Is there any way we could get the names/labels of that tuple. You can also suggest any other way.
Thanks.
###Edit:
Ok I after some research I found out that we cannot get the names of the parameters of the function. So now my goal is to shorten my code a little bit. In the above code where I am passing an object in place of Args. I only want to pass an array of strings.
const wrappedAdd = wrapper<typeof add, ["x", "y"]>(add);
I want to generate an object dynamically using this array. Thanks
I think it is better to define any types in global scope.
type Elem = any;
type Predicate<Key extends number, Value extends Elem> = Record<Key, Value>
type Reduce<
Arr extends ReadonlyArray<Elem>,
Result extends Record<string, any> = {}
> = Arr extends []
? Result
: Arr extends [infer H]
? Result & Predicate<0, H>
: Arr extends readonly [...infer Tail, infer H]
? Tail extends ReadonlyArray<Elem>
? Reduce<Tail, Result & Predicate<Tail['length'], H>>
: never
: never;
function wrapper<F extends (...args: any) => any>(func: F):
(...args: [Reduce<Parameters<F>>] | Parameters<F>) => ReturnType<F> {
return func
}
const add = (x: number, y: string, z: number[]) => x + y;
const wrappedAdd = wrapper(add);
const add = (x: number, y: string, z: number[]) => x + y;
const wrappedAdd = wrapper(add)({ 0: 1, 1: 'hello', 2: [1] }); // ok
const wrappedAdd2 = wrapper(add)(1, 'hello', [1]); // ok
Btw, no need to use type assertion here. I mean you can get rid of as operator
If you are interested in more examples of such kind of types, you can take a look on my article
Here you have representation of Reduce type in almost pure JS:
const reduce = <T,>(arr: T[], cache = {}) => {
if (arr.length === 0) {
return cache
}
if (arr.length === 1) {
const [head] = arr;
return { ...cache, [0]: head }
}
const [head, ...rest] = arr;
return reduce(rest, { ...cache, [rest.length]: head })
}
Playground
Like #jcalz said:
Parameter names are not observable in the type system except as documentation for IntelliSense
There is no way to infer parameter name in TS and put it into another type, so I decided to use index type.
0 for first argument, 1 for second, etc ...
Regarding using argument names:
Docs
There is one place where the differences begin to become observable though: readability.
They’re purely there for documentation and tooling.
const add = (x: number) => x + x
type Args = Parameters<typeof add>
type Infer = Args extends [infer R] ? R : never // number
type Infer2 = Args extends [x: infer R] ? R : never // number
Hence, I don't think it is possible in current version of TypeScript.

Typescript - How to infer generic in non-invoked function?

See the example below; it should be working by all reasonable interpretation of the code. Does anybody have any insight on why it doesn't?
TS-playground link
const func_returnsInput = (input: string) : Promise<string> => Promise.resolve(input);
type returnsInput_T = <T>(data: T) => Promise<T>;
const test: returnsInput_T = func_returnsInput;
**ERROR**
Type '(input: string) => Promise<string>' is not assignable to type 'returnsInput_T'.
Types of parameters 'input' and 'data' are incompatible.
Type 'T' is not assignable to type 'string'.(2322)
The type signature of func_returnsInput is:
const func_returnsInput: (input: string) => Promise<string>
That is specifically a function that accepts a string (and only a string) and returns a Promise<string>. You could change the implementation slightly and it would still conform to the annotated type signature:
const func_returnsInput = (input: string): Promise<string> =>
Promise.resolve(input.toUpperCase());
The fact that the implementation doesn't do that is not important to the compiler. It considers the call signature to be the externally-viewable type of the function, and any specific implementation to be hidden. If someone hands me func_returnsInput, all I know is that it takes a string and returns a Promise<string>. If I were to feed it, say, a number, then I have done something wrong.
Contrast that now with the type ReturnsInput_T:
type ReturnsInput_T = <T>(data: T) => Promise<T>;
That is a generic call signature with a generic type parameter T. The type parameter may be specified at will by the caller of the function, not by the implementer of the function. The type ReturnsInput_T is a function that accepts any value the caller wants (say, a number), and then returns a Promise of the same data type (so, Promise<number>).
If it helps, you can think of a generic call signature as an infinite intersection of all possible types for T. You can't write that out, but it behaves like
type ReturnsInput_T_Infinite =
& ((data: string) => Promise<string>)
& ((data: number) => Promise<number>)
& ((data: boolean) => Promise<boolean>)
& ((data: Date) => Promise<Date>)
& ((data: null) => Promise<null>)
// & ...
Now it hopefully makes sense why you cannot assign func_returnsInput to a value of type ReturnsInput:
const test: ReturnsInput_T = func_returnsInput; // error!
// Type 'T' is not assignable to type 'string'.
test(123); // no error
If test is truly of type ReturnsInput_T, then I should be allowed to call test(123), or test(false) or test(new Date()) or anything I want, and get back a Promise of the same data type. But func_returnsInput cannot be known to do that by the compiler (even if the implementation happens to do this).
And so you get an error!
Playground link to code
I think the problem is that you want to use the type of a function that takes a generic with a function that takes explicitly a string. The following works for me:
type returnsInput_T = <T>(data: T) => Promise<T>;
function func_returnsInput<T>(input: T) {
return Promise.resolve(input)
}
const test: returnsInput_T = func_returnsInput;
Hope it helped

What does the return type string|((args: ValidationArguments) => string) return?

This is taken from line 107 here. I think what this means is:
This function returns a string or a function that takes ValidationArguments as an argument and then returns a string.
But it's the first time I have ever seen something like this, so just want to make sure I'm reading it right?
static getMessage(type: string, isEach: boolean): string|((args: ValidationArguments) => string) {
Yes you are reading it right, it's called a union type. Union types allow the creation of types that can be either of the types in the union. You can use type-guards to narrow the types in a union:
let f : string|((args: ValidationArguments) => string);
if(typeof f === 'string') {
f // is string
}else{
f(null) // f is a function
}

Categories