Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using ES6 Map with Flow type

I'm trying to wrap my head around flow and I struggle to make it work with ES6's Map

Consider this simple case (live demo):

// create a new map
const m = new Map();

m.set('value', 5);

console.log(m.get('value') * 5)

flow throws:

console.log(m.get('value') * 5)
               ^ Cannot perform arithmetic operation because undefined [1] is not a number.
References:
[LIB] static/v0.72.0/flowlib/core.js:532:     get(key: K): V | void;
                                                               ^ [1]

I also tried:

const m:Map<string, number> = new Map();

m.set('value', 5);

console.log(m.get('value') * 5)

But I got the same error

I believe this is because flow thinks that the value can also be something else than a number, so I tried to wrap the map with a strict setter and getter (live demo):

type MyMapType = {
    set: (key: string, value: number) => MyMapType,
    get: (key: string) => number
};

function MyMap() : MyMapType {
    const map = new Map();

    return {
        set (key: string, value: number) {
          map.set(key, value);
          return this;
        },
        get (key: string) {
          return map.get(key);
        }
    }
}


const m = MyMap();

m.set('value', 5);

const n = m.get('value');

console.log(n * 2);

but then I got:

get (key: string) {
^ Cannot return object literal because undefined [1] is incompatible 
with number [2] in the return value of property `get`.
References:
[LIB] static/v0.72.0/flowlib/core.js:532:     get(key: K): V | void;
                                                               ^ [1]
get: (key: string) => number                            ^ [2]

How can I tell flow that I only deal with a Map of numbers?

Edit:

Typescript approach makes more senses to me, it throws on set instead on get.

// TypeScript

const m:Map<string, number> = new Map();

m.set('value', 'no-number'); // << throws on set, not on get

console.log(m.get('value') * 2);

Is there a way to make Flow behave the same way?

like image 896
Asaf Katz Avatar asked May 15 '18 16:05

Asaf Katz


2 Answers

What Flow is trying to tell you is that by calling map.get(key), .get(...) may (V) or may not (void) return something out of that map. If the key is not found in the map, then the call to .get(...) will return undefined. To get around this, you need to handle the case where something is returned undefined. Here's a few ways to do it:

(Try)

const m = new Map();

m.set('value', 5);

// Throw if a value is not found
const getOrThrow = (map, key) => {
  const val = map.get(key)
  if (val == null) {
    throw new Error("Uh-oh, key not found") 
  }
  return val
}

// Return a default value if the key is not found
const getOrDefault = (map, key, defaultValue) => {
  const val = map.get(key)
  return val == null ? defaultValue : val
}

console.log(getOrThrow(m, 'value') * 5)
console.log(getOrDefault(m, 'value', 1) * 5)

The reason that map.get(key) is typed as V | void is the map might not contain a value at that key. If it doesn't have a value at the key, then you'll throw a runtime error. The Flow developers decided they would rather force the developer (you and me) to think about the problem while we're writing the code then find out at runtime.

like image 52
James Kraus Avatar answered Oct 23 '22 22:10

James Kraus


Random and pretty late, but was searching and came up with this for my own use cases when I didn't see it mentioned:

const specialIdMap = new Map<SpecialId, Set<SpecialId>>();
const set : Set<SpecialId> = specialIdMap.get(uniqueSpecialId) || new Set();

and this saves quite a lot of boilerplate of checking if null and/or whatever. Of course, this only works if you also do not rely on a falsy value. Alternatively, you could use the new ?? operator.

like image 1
rob2d Avatar answered Oct 23 '22 22:10

rob2d