Skip to content
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

Flow does not accept intersection function types even with disjoint unions #3021

Open
danwang opened this issue Dec 15, 2016 · 12 comments
Open

Comments

@danwang
Copy link

danwang commented Dec 15, 2016

Hi!

I have the following code (try link)

type A = {
  type: 'a',
  a: string,
};

type B = {
  type: 'b',
  b: number,
};

type F = ((a: A) => string) & ((b: B) => number);
const foo: F = (ab: A | B) => {
  if (ab.type === 'a') {
    return ab.a;
  } else {
    return ab.b;
  }
}

I wish to make a function foo accepting a disjoint union but which returns different types for each member.

This fails with the following error:

14:     return ab.a;
               ^ string. This type is incompatible with
11: type F = ((a: A) => string) & ((b: B) => number);
                                             ^ number
16:     return ab.b;
               ^ number. This type is incompatible with
11: type F = ((a: A) => string) & ((b: B) => number);
                        ^ string

What's the best way to express this type of function (disjoint types input, but different output type for each one)? I would like to not have to declare it as (ab: A|B) => string|number because that loses some information.

Sincerely,
Dan

@danwang
Copy link
Author

danwang commented Jan 9, 2017

bump

@rhendric
Copy link
Contributor

See also: #4305, #2059, #60.

I would like to see this working. Declaring a function with an intersection type works great for usages of that function, but the actual function definition has to go through any and give up type checking, or be defined in a non-flow file (same effect).

@samwgoldman
Copy link
Member

I don't think this is a bug.

The program in the original post asks whether the following subtyping rule is true:

(A|B => X|Y) <: ((A => X) & (B => Y))

The above is not true, but the reverse is true.

@rhendric
Copy link
Contributor

I agree that the subtyping relation you wrote is not true, but I think the spirit of the bug is that there's no way to write foo so that Flow infers it to have the desired type F, explicitly or implicitly (without cheating). Is that not the case? I would be overjoyed if not.

@jcready
Copy link
Contributor

jcready commented Dec 22, 2017

Yeah, the feature request here is to get flow to accept a real javascript function as an intersection of function types. You can declare function intersections and you can cheat by first casting a function to any and then to a function intersection, but then you lose all guarantees that the function actually adheres to the intersection constraints. Flow should be able to verify that a real javascript function adheres to a function intersection type.

const foo: F = (ab) => {
  if (ab.type === 'a') {
    return ab.a;
  } else {
    return ab.b;
  }
}

@zeorin
Copy link

zeorin commented Jul 11, 2018

I noticed something interesting recently:

// @flow

interface T {
	(t: number): number;
	(t: string): string;
}

declare var returnTypeInterface: T;

(returnTypeInterface('string'): string);
(returnTypeInterface(2): number);

// $ExpectError
(returnTypeInterface('string'): number);
// $ExpectError
(returnTypeInterface(2): string);

(Flow Try)

This seems to work. But I'm not sure if it's safe.

@apsavin
Copy link
Contributor

apsavin commented Aug 31, 2018

@samwgoldman @mrkev can we reopen this issue?

@DeedleFake
Copy link

I think an issue I'm having is related to this, so I'm asking about it here:

I'm trying to get a function to return different, but related, types based on the type of the input. For example:

type Arg = { +[string]: Object }
type CallbackArg = () => Arg
type Func = <T: Arg | CallbackArg>(T) => /* Here's where I'm stuck. */

What I'd like to do is return { +[$Keys<T>]: string } if the object is passed directly and { +[$Keys<$Call<T>>]: string} if a function is passed. Is there any way to do this type of conditional overloading? I haven't been able to find anything obvious via either the documentation or Google. I'm essentially looking for some kind of $IfCompatible<T, Arg, { +[$Keys<T>]: string }, { +[$Keys<$Call<T>>]: string }>, but a feature like that doesn't seem to exist. Am I going about this completely wrong?

For clarification, by the way, I'm dealing with this inside a type declaration file, so I can't write some kind of utility function to handle the conditional for me via ifs and a $Call<>.

I was able to get it mostly working via

type Func = <T: Arg | CallbackArg>(T) => { +[$Keys<T> | $Keys<$Call<T>>]: string }

but it seems kind of awkward. I'm also not sure why this would work, as I would think Flow would complain about $Call<T> making no sense if T is Arg. For the record, using $Keys<T | $Call<T> does not work.

(Try)

@rhendric
Copy link
Contributor

An interface is probably your best bet: Try

@DeedleFake
Copy link

DeedleFake commented Mar 25, 2019

Huh. That should probably be in the documentation.

Oddly, the order seems to affect it. If the variant that takes () => T is listed in the interface definition second, it'll fail again.

Edit: And I've just noticed that @zeorin was talking about the same thing. Totally didn't notice because I didn't even think that an interface would have anything to do with this particular problem. Woops.

@FireyFly
Copy link
Contributor

Interesting question! An interface is probably cleanest, I didn't realise they work that way either... good to know. I think it essentially desugars to an intersection type; this (try) is what I came up with when experimenting.

The relevant bit being:

type ObjectToString = Object => string;
type Func =
  & (<T: Arg>(() => T) => $ObjMap<T, ObjectToString>)
  & (<T: Arg>(T) => $ObjMap<T, ObjectToString>);

@DeedleFake wrt order, I believe they're tried left-to-right (or top-down in the case of multiple declarations in an interface I suppose), so you probably want to put more specific types first.

@zeorin
Copy link

zeorin commented Mar 28, 2019

I've also been using intersection function types for this recently. I wish the docs mentioned this behaviour.

I figure an intersection object type is really used to check whether something is more than one (inexact) object type at the same time.

So for an intersection function type, it's really more than one function type at the same time. Of course, in my own code that's usually a good sign I'll be better off just creating individual functions instead, but sometimes I have to type other people's code, Redux + Redux Thunk's dispatch being a good example.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests