flowtype: how can i overload function return types by argument count/types?
Asked Answered
U

2

6

i want to define an overloaded function like

function first(n?: number) {
  if (number === undefined) {
    // returns a single Item
    return items[0];
  }

  // returns an array of Item
  return items.slice(0, n);
}

so that these statements type check:

const item: Item = first(); // no args, so return type is Item
const items: Array<Item> = first(5); // number arg, so return type is Array<Item>

flow knows that the first call to first is going to result in n === undefined (since it would complain if undefined wasn't valid for n) and it understands that it will then take the if branch, so i would think it could infer the return type is Item, but everything i've tried either lets anything pass or always fails.

any idea if this is possible? thanks in advance internet.

Urban answered 1/3, 2017 at 18:23 Comment(0)
N
5

I don't have a complete solution for you, but I got partway there:

const items = [1, 2, 3];

type FirstType = ((_: void) => number) & ((n: number) => Array<number>);

const first: FirstType = (n?: number) => {
  if (n === undefined) {
    // returns a single Item
    return (items[0]: any);
  } else {
    // returns an array of Item
    return (items.slice(0, n): any);
  }
}

const a: number = first();
const b: Array<number> = first(2);

(tryflow)

The & is an intersection type, and it means that first must satisfy both of those types. You can see that the calls to first() typecheck the way you want.

Unfortunately, it does not seem like Flow is currently able to typecheck the body of first. Note that I had to cast the return values through any to escape the typechecker. If you are willing to forgo typechecking in the body of your function, you can at least get it where the functions are called.

Nickienicklaus answered 1/3, 2017 at 19:16 Comment(2)
yeah... that works! i can't say i really understand why you need the any cast... but it works. thanks. and i didn't know about that "tryflow" page either, that's super useful for fiddling.Urban
Just a warning though, casting through any is unsafe. You won't get typechecking for the return values.Nickienicklaus
G
1

This may not work in the version of Flow that was current when you wrote OP, but trying against the latest version, I just discovered you can use declare function even in source files to avoid the horrific syntax of ((_: void) => number) & ((n: number) => Array<number>).

type Item = number;
const items: Array<Item> = [0, 1, 2, 3, 4];

declare function first(): Item;
declare function first(n: number): Array<Item>;
function first(n?: number): Item | Array<Item> {
  if (n === undefined) {
    // returns a single Item
    return items[0];
  }

  // returns an array of Item
  return items.slice(0, n);
}

const item: Item = first(); // no args, so return type is Item
const items2: Array<Item> = first(5); // number arg, so return type is Array<Item>

Try Flow link

For class methods, the only way I've found is to declare the field type separately with the ugly & syntax (doesn't matter whether you use declare before the field declaration or not):

class Foo {
  foo: ((x: number) => string) & ((x: string) => number)
  foo(x: number | string): number | string {
    if (typeof x === 'number') return String(x)
    else return Number(x)
  }
}

const foo = new Foo()
const a: number = foo.foo('1')
const b: string = foo.foo(2)

Try Flow link

Genital answered 3/2, 2021 at 5:14 Comment(1)
Worth noting that when you use declare inline like that, flow will assume your function abides by the declared signature; so if your actual function is changed in a way that changes the signature, flow won't report an errorTowill

© 2022 - 2024 — McMap. All rights reserved.