facebook / flow

Adds static typing to JavaScript to improve developer productivity and code quality.
https://flow.org/
MIT License
22.09k stars 1.86k forks source link

Allow refinement of exact object union types with typeof #4639

Open mike-marcacci opened 7 years ago

mike-marcacci commented 7 years ago

Refining exact object union types by shape is currently VERY limited. We should be able to refine such types by their shape using typeof at the very least, and ideally any other common refinement techniques (Array.isArray, etc).

From my perspective this should work:

// @flow

type Foo = {|foo: string|};
type Bar = {|bar: string|};
type SomeUnion = Foo | Bar;

function getFooOrNull (u: SomeUnion): ?Foo {
    return typeof u.foo === 'undefined' ? null : u;
}

but fails with:

4: type Bar = {|bar: string|};
              ^ property `bar`. Property not found in
7: function getFooOrNull (u: SomeUnion): ?Foo {
                                          ^ object type
7: function getFooOrNull (u: SomeUnion): ?Foo {
                                          ^ property `foo`. Property not found in
4: type Bar = {|bar: string|};
              ^ object type

This limitation is also described in this comment, and is related to #4328.

mhelvens commented 6 years ago

Or with if ('foo' in u). Or with if (u['foo']) (because some keys require that notation).

mgtitimoli commented 6 years ago

You need to use disjoint unions, that basically ask you to add a common property between all the types that compose the union with an unique value on each of them, and then use this property to refine.

Example

mhelvens commented 6 years ago

@mgtitimoli: Yes, that's how it currently needs to be done. But this is a feature request to overcome that limitation.

First of all, Foo | Bar is already a disjoint union, in the sense that there can exist no value that is a member of both types. Therefore, typeof u.foo !== 'undefined' should be enough here to conclude that u typeof Foo, by a simple deduction. It would be cool if Flow could make that deduction.

rickyp-uber commented 5 years ago

I ran into a similar situation today so I wrote up a flow-try: https://flow.org/try/#0KYDwDg9gTgLgBDAnmYcCCcC8cDeBDALjgGcYoBLAOwHMBfAbgChRJYFlUAhLXAIyNIUaDRs3DR4SFOgA2MrIzhwAPnAAkAURB4AxjAA8agErA8AEwDylGYn1oAfPcUr1W3QeOnL1250dNGADMAV0o9cghKOGpgGABJMwAKPGCYAAtoIjQ5AEoBMipqXGcoWOCoKJT06AA6PBVVKoyoGt4mWiA

export type A = {a: string};
export type B = {b: string};

export type All =
  | $Exact<$ReadOnly<A>>
  | $Exact<$ReadOnly<B>>;

function getId(author: All): string {
  return author.a || author.b;
}

Basically the union type only has two options, so if the first is not satisfied then the second is the only option left and will return a string.

SleepWalker commented 5 years ago

Disjoint unions with exact types will work only if you provide literals. If you provide some more generic types like string, number, boolean — it will fail

If this is not a bug, than the docs here are not enough accurate. There should be info, that it works only with literals

Here is an example from docs, where true was replaced with boolean and everything breaks:

https://flow.org/try/#0PTAEAEDMBsHsHcBQAXAngBwKagMoFcBjAzAZxNAF5QBvAH1BMOLIC5QAjWWaTAQwDsANKABuvaHkxtO3Pv1C0AvgG4UGbADFeASx4ATUJRr1MAJ1OxT0rjwHCAtqRK8A5lIbJT2-i4UrEaligAEqk6LD8JNhU+EROCqBauph6qoiQePwEyNoRoAAWAno8oSThkZgAFKZhEVFspeVRAJQ0iIbakKDVtRUAdIxxZK3U7YaivKYTEu4ytvJUNWV1mH1iM6qGiqCY0FFt4xNTjmSu7iSe3r6LvVF9J85um6CKiIpAA

FlavienBusseuil commented 3 years ago

Disjoint unions with exact types will work only if you provide literals. If you provide some more generic types like string, number, boolean — it will fail

It's not about literals, it's about types that could be "falsy" (as string could be "", boolean false and number 0). As type refinement works on values and not keys it make sense that the refinement can't be done with string, boolean and number.

Look again this example and think what's going on with message === ""

type Success = {| message: string |};
type Failed  = {| error: string |};

type Response = Success | Failed;

function handleResponse(response: Response) {
  if (response.message) {
    return response.message;
  } else  {
    // I think Flow is right, refinement can't be done here, message could exists but empty!
    return response.error; // Cannot get `response.error` because property `error` is missing in `Success`
  }
}

I guess what we need is a way to do type refinement that works by testing the keys of an object as @mhelvens suggested.