Flow: [Feature] $Arguments<func>

Created on 21 Aug 2017  路  9Comments  路  Source: facebook/flow

Sometimes it's necessary to accept the same arguments as another function but provide a different return type.

Having an $Arguments<> utility would help avoid boilerplate.

Given:

function funcOne(string, number, string): string {
    let result = "";
    // magic ...
    return result;
}

type FuncOneType = typeof funcOne;

Right now, the way to define a function type with the same arguments but void return type requires defining a function that will never be used:

const funcOneButReturnsVoid = (...args) => {funcOne(...args)};
type FuncOneTypeReturnsVoid = typeof funcOneButReturnsVoid;

With $Arguments it would be:

type FuncOneTypeReturnsVoid = $Arguments<funcOne> => void;

There's probably some way to achieve this, but I went through a lot of resources and flow's source code before coming here. If there is, I apologize in advance.

Most helpful comment

@lkoba maybe this declaration could help?

https://goo.gl/FVzFVc

declare function arguments<A>((A) => any): [A]
declare function arguments<A, B>((A, B) => any): [A, B]
declare function arguments<A, B, C>((A, B, C) => any): [A, B, C]

type Arguments<T> = $Call<typeof arguments, T>

type Fn1 = (?string) => void
type Fn2 = (string, number) => void

let a

;((a = [1]): Arguments<Fn1>) // error
;((a = [undefined]): Arguments<Fn1>)
;((a = ['x']): Arguments<Fn1>)
;((a = ['a', 1]): Arguments<Fn2>)

and with your example:

function funcOne(a: string, b: number, c: string): string {
    let result = "";
    // magic ...
    return result;
}

type FuncOneTypeReturnsVoid = (...args: Arguments<typeof funcOne>) => void;

const x: FuncOneTypeReturnsVoid = (a, b) => undefined

x('x', 1, 'x') // ok
x('x', 1, 1) // error

All 9 comments

Hey there! Can you give some more context on the situation where you'd want to do this? In my experience, when two functions share an input type, it's usually because that type is meaningful elsewhere in my program, so I give it a name and refer to that name in both function declarations. I'm trying to think of why I'd want one function to have the same argument types as another but not give them names, and I can't think of one, but I'd love to hear more about your use case.

Also, I was curious about your second block of code, and after poking at it, I don't think it does what you describe. When I use that code and ask flow what type FuncOneTypeReturnsVoid is, it says: (...args: any[]) => void. Flow hasn't made up a new type with the same argument types, it's just thrown up its hands and said "it can be anything".

I don't think there is currently any way to represent the idea of "the types of the arguments to a certain function". But I'd love to hear more about why you need that and maybe we can come up with a creative solution.

I'm using the container/component pattern with React and Redux.

Let's say we have a Component that expects a function that opens a Product's page. The properties it expects are defined like this:

type Props = {
   openProductPage: (productId: string) => void,
}

There's usually an Action creation function that most of the time matches the arguments exactly, but it returns an Action instead. The Container dispatches this Action to be processed by reductors, sagas, etc. The signature would be openProductPage(productId: string) => Action. You can see that it's almost the same, but the return type changes. This is not an issue because the call is wrapped by the Container (productId) => dispatch(openProductPage(productId)) when sent to the Component. The signature matches and everyone is happy.

Now. The issue here is that replicating the function arguments on each Props where it's expected is a maintenance problem. If you change an argument in an action creator, you have fix dispatch wrappers, then the Props and then the Component. We automated the dispatch wrapping to simplify one step and we would love to be able to simplify the Component properties definition doing something like:

type Props = {
   openProductPage: $Arguments<actions.openProductPage> => void,
}

This way, if we modify the action creation function arguments, we can skip directly to fixing the code.

The alternative for my use case is to encapsulate all arguments for each action creator in an object and define a type for each one, that way the Component could expect something like:

type Props = {
  openProductPage: (OpenProductOptions) => void
}

Still, $Arguments would be less traumatic so I wanted to make sure that it wasn't available in other form.

@lkoba maybe this declaration could help?

https://goo.gl/FVzFVc

declare function arguments<A>((A) => any): [A]
declare function arguments<A, B>((A, B) => any): [A, B]
declare function arguments<A, B, C>((A, B, C) => any): [A, B, C]

type Arguments<T> = $Call<typeof arguments, T>

type Fn1 = (?string) => void
type Fn2 = (string, number) => void

let a

;((a = [1]): Arguments<Fn1>) // error
;((a = [undefined]): Arguments<Fn1>)
;((a = ['x']): Arguments<Fn1>)
;((a = ['a', 1]): Arguments<Fn2>)

and with your example:

function funcOne(a: string, b: number, c: string): string {
    let result = "";
    // magic ...
    return result;
}

type FuncOneTypeReturnsVoid = (...args: Arguments<typeof funcOne>) => void;

const x: FuncOneTypeReturnsVoid = (a, b) => undefined

x('x', 1, 'x') // ok
x('x', 1, 1) // error

@lttb's example is very helpful, but doesn't work for functions with 0 arguments. I tried to make it do that in this try flow, but I'm not sure if it fully works.

Also, see this example for wrapping in a promise:

// try to wrap in promise

function returnOne (): number {
      return 1
}

function addOne (num: number): number {
      return num + 1
}

type ExtractReturnType = <R, A>((...args: Array<A>) => R) => R
type FuncWrappedInPromise<F> = (...args: Arguments<F>) => Promise<$Call<ExtractReturnType, F>>;

const y: FuncWrappedInPromise<typeof returnOne> = () => new Promise((resolve) => {resolve(returnOne())})
const z: FuncWrappedInPromise<typeof addOne> = (val) => new Promise((resolve) => {resolve(addOne(val))})

y().then(num => num + 1) // should be ok, but is not :(
z(2).then(num => num + 1) // ok
z(2).then(num => num.substr(1)) // error

Hi @evansiroky!

Excuse me for that example, I just missed the declaration without the arguments there.
This is the correct implementation:

declare function arguments<A>(() => any): []
declare function arguments<A>((A) => any): [A]
declare function arguments<A, B>((A, B) => any): [A, B]
declare function arguments<A, B, C>((A, B, C) => any): [A, B, C]
declare function arguments<A, B, C, D>((A, B, C, D) => any): [A, B, C, D]
declare function arguments<A, B, C, D, E>((A, B, C, D, E) => any): [A, B, C, D, E]

Try here

You might also find this type useful: https://github.com/lttb/flown/blob/master/src/Arguments/index.js
It's a bit more complex, but the base idea is the same

@lttb I came up to the same solution however it doesn't work for optional arguments in the function.

Try here

Following up on this, here's a version that supports any number of arguments:

type $Arguments<F> = $Call<
  <A: $ReadOnlyArray<mixed>>((...A) => mixed) => A,
  F
>;

Solution above doesn't seem to work

https://flow.org/try/#0AQFwngDgpsAkCCAnA5gVwLZQHYgM4B4AxAPmAF44BhAQwBtb8AoYYfeALjgCUpqATAPJZaYJImph86AJYAPKH2LEAFMoB0G+AEpypGfL46ypeABpmwQo2IBuRvYBmqLAGMQ0gPZZg-QVhjKWBicQegARlCIWiEYEYjAAN4WLCyIUCCoiN6hwADUwACMjAC+9owuXrggPii4BZwIKBjYePjg0B4OPnx+UKQUAOTUA0A

@lttb I came up to the same solution however it doesn't work for optional arguments in the function.

Try here

+1 on this, solution seems to require arguments that are optional in the function signature

I ran into a case where this approach seems lossy somehow when combined with spread syntax

(It may be unrelated or I'm doing something dumb, but still, fwiw: https://stackoverflow.com/questions/57847208/how-can-i-get-the-arguments-to-a-function-in-a-non-lossy-way-with-flow)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

MarcoPolo picture MarcoPolo  路  67Comments

StoneCypher picture StoneCypher  路  253Comments

danvk picture danvk  路  73Comments

STRML picture STRML  路  48Comments

Gozala picture Gozala  路  54Comments