In JavaScript, it's common to have a function that may be called in more than one way – e.g. with handful of positional arguments or a single options object or some combination of the two.
I've been trying to work out how to annotate this.
One way I tried was to annotate rest args as a union of various possible tuples:
type Arguments =
| [string]
| [number]
| [string, number]
;
const foo = (...args: Arguments) => {
let name: string;
let age: number;
// unpack args...
if (args.length > 1) {
name = args[0];
age = args[1];
} else if (typeof args[0] === 'string') {
name = args[0];
age = 0;
} else {
name = 'someone';
age = args[1];
}
console.log(`${name} is ${age}`);
};
// any of these call signatures should be OK:
foo('fred');
foo('fred', 30);
foo(30);
The above snippet is contrived; I could probably just use (...args: Array<string | number>) in this example, but for more complex signatures (e.g. involving a typed options object that can be alone or with prior args) it would be useful to be able to define a precise, finite set of possible call signatures.
But the above doesn't type-check. You can see a bunch of confusing errors in tryflow.
I also tried typing the function itself as a union of separate entire function defs, but that didn't work either:
type FooFunction =
| (string) => void
| (number) => void
| (string, number) => void
;
const foo: FooFunction = (...args) => {
let name: string;
let age: number;
// unpack args...
if (args.length > 1) {
name = args[0];
age = args[1];
} else if (typeof args[0] === 'string') {
name = args[0];
age = 0;
} else {
name = 'someone';
age = args[1];
}
console.log(`${name} is ${age}`);
};
// any of these call signatures should be OK:
foo('fred');
foo('fred', 30);
foo(30);
How should I approach type-annotating functions with multiple possible call signatures? (Or are multi-signatures considered an anti-pattern in Flow, and I just shouldn't be doing it at all – in which case, what is the recommended approach for interacting with third party libraries that do it?)
The errors you are seeing are a combination of a bug in your code and a bug in Flow.
Bug in your code
Let's start by fixing your bug. In the third else statement, you assign the wrong value to
} else {
name = 'someone';
age = args[1]; // <-- Should be index 0
}
Changing the array access to be the correct index removes two errors. I think we can both agree this is exactly what Flow is for, finding errors in your code.
Narrowing type
In order to get to the root cause of the issue, we can be more explicit in the area where the errors are so that we can more easily see what the problem is:
if (args.length > 1) {
const args_tuple: [string, number] = args;
name = args_tuple[0];
age = args_tuple[1];
} else if (typeof args[0] === 'string') {
This is effectively the same as before but because we're very clear about what args[0] and args[1] should be at this point. This leaves us with a single error.
Bug in Flow
The remaining error is a bug in Flow: https://github.com/facebook/flow/issues/3564
bug: tuple type is not interacting with length assertions (.length >= 2 and [] | [number] | [number, number] type)
How to type overloaded functions
Flow is not great at dealing with variadics with different types, as in this case. Variadics are more for stuff like function sum(...args: Array<number>) where all the types are the same and there is no maximum arity.
Instead, you should be more explicit with your arguments, like so:
const foo = (name: string | number, age?: number) => {
let real_name: string = 'someone';
let real_age: number = 0;
// unpack args...
if (typeof name === 'number') {
real_age = name;
} else {
real_name = name;
real_age = age || 0;
}
console.log(`${real_name} is ${real_age}`);
};
// any of these call signatures should be OK:
foo('fred');
foo('fred', 30);
foo(30);
This causes no errors and I think is just easier to read for developers, too.
A better way
In another answer, Pavlo provided another solution that I like more than my own.
type Foo =
& ((string | number) => void)
& ((string, number) => void)
const foo: Foo = (name, age) => {...};
It solves the same problems in a much cleaner way, allowing you much more flexibility. By creating an intersection of multiple function types, you describe each different way of calling your function, allowing Flow to try each one based on how the function is called.
You can define multiple function signatures by joining them with &:
type Foo =
& ((string | number) => void)
& ((string, number) => void)
Try it.
Of the three possible workouts you gave, I've figured out how to make it work using an single options object, however because you require at least one object to be set, you need to define each possibility.
Like this:
type Arguments =
{|
+name?: string,
+age?: number
|} |
{|
+name: string,
+age?: number
|} |
{|
+name?: string,
+age: number
|};
const foo = (args: Arguments) => {
let name: string = args.name ? args.name : 'someone';
let age: number = typeof args.age === 'number' && !isNaN(args.age) ? args.age : 0;
console.log(`${name} is ${age}`);
}
// any of these call signatures are OK:
foo({ name: 'fred' });
foo({ name: 'fred', age: 30 });
foo({ age: 30 });
// fails
foo({});
Related
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
This question already has an answer here:
Function parameter type depends on another parameter
(1 answer)
Closed 6 months ago.
Can somebody please help me. How can I run this typescript code without issues at type checking?
type FuncsType = {
name1(argv1: string, argv2: string): void;
name2(argv1: number, argv2: number): void;
};
const functions: FuncsType = {
name1: (argv1, argv2) => {
console.log(argv1, argv2);
},
name2: (argv1, argv2) => {
console.log(argv1, argv2);
}
};
type start = (name: keyof FuncsType, ...args: Parameters<FuncsType[keyof FuncsType]>) => void;
const start: start = (name, ...args) => {
const func = functions[name] as FuncsType[keyof FuncsType];
func(...args);
};
start("name1", "1", "2");
start("name2", 3, 2);
error
I found out that the problem no longer occurs when I specify the rest arguments of a certain function.
success hack
So, in principle you could just do the following and get the same result with the benefit of being less verbose and clearer about your intentions.
function start(args: number[] | string[]): void {
console.log(args);
}
start(['1', '2']);
start([3, 2]);
I personally prefer to provide an array as an argument and be explicit about what it is I am providing, instead of saying: "Here. Take the remaining arguments", which is what you are doing if you're using the spread operator.
Now let's say you want to do something more than just console.log() something and you actually want to call a separate function based on a condition. I understand your desire to put this in an object, but I'd personally just make check for the contents of your args like this:
function start(args: number[] | string[]): void {
args.forEach(arg => {
if (arg === typeof 'string') {
// do something
}
if (arg === typeof 'number') {
// do something else
}
});
}
I'm personally not a fan of using objects to look up functions...
In my opinion you also don't need to type your functions. So I wouldn't worry too much about doing things like this:
const start: start = () => {}
I would however advise on communicating a functions return type, so in this case omitting the function type and showing the return type:
const start = (): void => {}
TypeScript is about readability for me, so I try to be clear and communicate with others (and my future self) by using types, but I try to avoid complex generics and unions until I really need them.
Now, a quick word about the spread operator (...) for arrays in JavaScript:
const array = [1,2,3,4,5]
console.log(array) // [1,2,3,4,5]
is exactly the same as saying:
const array = [1,2,3,4,5]
console.log([...array]) // [1,2,3,4,5]
Note: If you don't provide the square brackets, you will actually just get the numbers and NOT an array.
const array = [1,2,3,4,5]
console.log(...array) // 1 2 3 4 5
On a sidenote Optional Parameters or types can be marked with a '?'
as in:
class SomeClass
myVariable1?: string; // can be omitted when calling the constructor new
myVariable2: Number; // SomeClass(1) or not new SomeClass("somestring", 2);
constructor(omittableConstructorParameter1?:string,
notOmittableConstructorParameter2:Number) {
this.myVariable1 = ommitableConstructorArgument;
this.myVariable2 = notOmmitableConstructorArgument;
}
this concept applies to Interfaces, too (ofc without the constructor)
function identity<String>(arg: String): String {
return arg;
}
let myIdentity = identity([2]);
console.log()
Hi could someone help me to understand why this doesn't throw any type error even if i am passing a array of numbers ?
Is it because the type is "String" instead of "string" , which looks for objects than primitive ?
If answer is "yes" , if i change everything to string i get error saying string is never used
function identity<string>(arg: string): string {
return arg;
}
let myIdentity = identity([2]);
console.log(myIdentity )
'string' is declared but its value is never read.
You are shadowing the builtin types String and string by using their names as your generic parameter. Just use a parameter name that is not a built-in to solve it:
TS Playground link
function identity<T extends string>(value: T): T {
return value;
}
identity([2]); // error [2] doesn't extend string
identity('2'); // ok
If you want a generic identity function without any kind of constraint, you can use an unconstrained type parameter:
TS Playground link
function identity<T>(value: T): T {
return value;
}
const numberArrayValue = identity([2]); // number[]
const objectValue = identity({b: 2}); // { b: number; }
const stringLiteralValue = identity('2'); // "2"
const numberLiteralValue = identity(2); // 2
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.
I usually try to keep flow function types separate from their implementation. It's a slightly more readable when I write:
type Fn = string => string;
const aFn: Fn = name => `hello, ${ name }`;
rather than:
const aFn = (name: string): string => `hello, ${ name }`;
When using generic types we can write:
const j= <T>(i: T): T => i;
const jString: string = j('apple'); // √
const jNumber: number = j(7); // √
But how can I separate this type from a function expression?
type H<T> = (input: T) => T;
const h:H<*> = i => i; // --> WHAT SHOULD GO FOR '*'?
const hString: string = h('apple'); // X error
const hNumber: number = h(7); // X error
What should be used for *? any would work but that's not what I want.
In haskell this is a non-issue:
identity :: a -> a
identity a = a
identity "a-string" // √
identity 666 // √
See flow.org/try
So I have noticed that if I use bounded generics, it'll work:
type H<T> = <T: *>(input: T) => T;
const h:H<*> = i => i;
const a: string = h('apple'); // √
const b: number = h(7); // √
const c: {} = h({ nane: 'jon' }); // √
Don't ask me WHY.
type H<T> = (input: T) => T;
const h2:H<*> = i => i;
const h3:H<*> = i => i;
const hString: string = h3('apple');
const hNumber: number = h2(7);
I think the underlying issue with your code and type definitions is based on a misunderstanding of a particular property of generics (aka parametric polymorphism): Parametricity.
Parametricity states that a generic function must not know anything about the types of its polymorphic arguments/return value. It is type agnostic.
As a result, a generic function must treat each value associated with a polymorphic type equally, regardless of the concrete type. This is pretty limiting, of course. When a function doesn't know anything about its argument, it can't do anything with it other than returning it unchanged.
Let's have a look at an incorrect generic function:
const f = <a>(x :a) :a => x + "bar"; // type error
Try
f doesn't type check as expected because f must not treat x as a String. Flow enforces parametricity.
Please note that generics are much more useful in connection with higher order functions. Here is an example of a correct generic higher order function:
type F<X, Y> = (x: X) => Y;
const apply = <X, Y>(f: F<X, Y>, x: X) :Y => f(x);
const sqr = (x :number) :number => x * x;
const toUC = (s :string) :string => s.toUpperCase();
apply(sqr, 5); // does type check
apply(toUC, "foo"); // does type check
Try
Why would you define a specific version of apply for every possible type? Just apply it to values of arbitrary type provided that the types of f and x are compatible.
Conclusion
When you have an unbounded generic function, you can apply it to whatever value you want - it always works as expected. So there isn't really a reason to declare a distinct function for each possible type. Smart people have invented polymorphism in order to avoid exactly this.
A problem remains, though. As soon as you separate the generic type definition form the function declaration, Flow no longer enforces parametricity:
const f = <a>(x :a) :a => x + "bar"; // type error
type G<a> = a => a;
const g :G<*> = x => x + ""; // type checks!?!
Try
So your question is still reasonable and unfortunately, I can't offer you a recipe. Hopefully, Flow's behaviour will change in future versions.
In your own answer, you suggest using bounded generics. I wouldn't do that because it solves a problem that doesn't exist at all and because it seems to be a misuse of this sort of polymorphism.