Flow: When using Object as maps, Object.values treats values as mixed

Created on 9 Aug 2016  ยท  65Comments  ยท  Source: facebook/flow

I'm using objects as maps in the example below. It appears the type of Object.values is an array of mixed as opposed to an array of the Point type I defined.

type Point = {
  x: number,
  y: number,
};
type PointMap = { [key:string]: Point }

const foo: PointMap = { "1": { x: 5, y: 6}, "2": { x: 7, y: 8} };

Object.values(foo).map(p => {
  return p.x;
})

It appears if I do use Object.keys instead, flow can correctly infer the type of foo[key]

Object.keys(foo).map(key => {
  return foo[key].x;
})

This seems like a bug in flow, as we use Object as a map here and the type checker has enough info to correctly infer the type of Object.values(). Yes?

enhancement

Most helpful comment

The 2-year anniversary of this issue is approaching! ๐ŸŽ‰

Nobody uses Object.values at Facebook?

All 65 comments

Why does Object.values return Array<mixed> currently?

At the moment, if you have a variable obj with the type { foo: string }, what does that mean? It means that obj.foo has the type string. But it doesn't mean that obj only has a single property. There may be other properties. This is because we let you write code like this:

function foo(obj: { foo: string, bar: number }) : { foo: string } {
  return obj;
}

Allowing subtypes to have fewer properties than super types makes structural subtyping with object types work well. But it means when you write Object.values(obj), we can't say with certainty what types are in the resulting array.

How do I specify an object with ONLY certain properties?

As of v0.30.0, there is no way. However, @bhosmer is here to save the day! He's in the middle of landing the $Exact magic type. $Exact<{foo: string}> is the type of an object who has exactly one property named foo that is a string.

How does this help with Object.values?

Building on top of this, @samwgoldman is updating the types of Object.{entries,keys,values} to offer better types when used with exact types. So Object.keys(obj) should return Array<'foo'> when obj has the type $Exact<{foo: string}>. This might make v0.31.0, but if it doesn't it will definitely make v0.32.0.

I think this and https://github.com/facebook/flow/issues/2174 are duplicates.

@gabelevi Does what you're saying apply for map types, though? It seems like maps don't have this issue? As in the following gives me a type error on a:

const a: {[key: string]: string} = {b: 3};
const b: {a?: string} = {b: 3};

Could this just be a matter of adding an overload for the case where the argument to Object.values or Object.entries is a map type?

Having this exact same problem right now.

Use case:

// types.js
export type FieldLabel =
    'SOME_FIRST_LABEL'
  | 'SOME_SECOND_LABEL'
  // ...
;

// constants.js
import type { FieldLabel } from './types';

export type FieldLabels = { [key: FieldLabel]: FieldLabel };
export const FIELD_LABELS: FieldLabels = Object.assign({}, ...[
  'SOME_FIRST_LABEL',
  'SOME_SECOND_LABEL',
  // ....
].map(label => ({ [label]: label })));


// elswhere, someFile.js
import type { FieldLabel } from '../types';

import { FIELD_LABELS } from '../constants';

const fieldLabels: Array<FieldLabel> = Object.values(FIELD_LABELS).sort();

Results in: string enum This type is incompatible with (:0:1,0) mixed (/private/tmp/flow/flowlib_28d43509/core.js:52:39,43) [javascript/flow]

Another simple reproducible case:
https://flowtype.org/try/#0PQKgBAAgZgNg9gdzCYAoVAXAngBwKZgAKMAhgMYEC8YA3qmGAG4kwCueAXGAM4YBOASwB2Ac1QBfdNnxFSFAMLkAFlVr0wAbQEATLr0GiAul2Lk8E9GThDeRADIBBeQFEA+vKcAJZybl5FZCpg1DTiANyW1rbMbKoA5Bh4vHERqISOLu5ezhoJSRhxhsG0TCzsXHFQcHBxYOHoAPIARgBWeGQYAHQx7NwAFOlObh7y3gCUnVDC2n19OH6+ZmOLFMEAfGoMW3x4GKx8QmDzZt1lVJTUPXgR4mNhQA

Another very simple reproducible case, just in case it helps: https://flowtype.org/try/#0PQKgBAAgZgNg9gdzCYAoALgTwA4FMwCSAJmALxgDO6ATgJYB2A5gNwY74Byc6tUtAxgEMecemTABvVGDC0iALkJEANNLABbXBQqDGuRVTpNVAX1Zs8YAMrph+cgBIAogA9B-dAB4JAbQDWuJiKxAC6ilw8fEIi9CYAfOb8olSUtuj61mn2kmaoqEn0KfTcvALCtKLhJVHlouISsgpgAOQAjM3KGlo6eorNcLTNYLlUdj7FkWUxAHRyIeITpdEV9InJ6GCLNTEUVZPLoj7z5ADyAEYAVrge0wBugjAArloAFKPpAJSsQA

I gave using $Exact<> an attempt here but looks like I might be using it wrong?

When I try to use exact notation to map

/* @flow */

const a: {|[string]: string|} = {'a': '1'};
const values: Array<string> = Object.values(a);

I get these errors

3: const a: {|[string]: string|} = {'a': '1'};
                                   ^ property `a`. Property not found in
3: const a: {|[string]: string|} = {'a': '1'};
            ^ object type
4: const values: Array<string> = Object.values(a);
                       ^ string. This type is incompatible with
[LIB] static/v0.42.0/flowlib/core.js:52:     static values(object: any): Array<mixed>;
                                                                               ^ mixed

Can you please tell me

  1. Why does exact notation not working with _Object.values_ in this case?
  2. Why is it not possible to declare some variable as object map and then assign an object literal to it?

Type declarations like {|[string]: string|} are not really useful. Exact implies that all properties are known in advance.

@vkurchatkin Yes, that makes sense. But what how to deal with Object.values returning Array<mixed> for {[string]: string}?

How the below should be written? Even when everything is exact, that doesn't work with 0.45.0

type Thing = {|name: string, b: number|};
type Things = {|[id: string]: Thing|};
const getSortedArray = (things : Things) => Object.values(things).sort((a, b) => a.name.localeCompare(b.name));

what we get:

216: const getSortedArray = (things : Things) => Object.values(things).sort((a, b) => a.name.localeCompare(b.name));
                                                                                                             ^^^^ property `name`. Property cannot be accessed on
216: const getSortedArray = (things : Things) => Object.values(things).sort((a, b) => a.name.localeCompare(b.name));
                                                                                                           ^ mixed

This is causing headache too often.

Any solution for this? Running into this all afternoon :(

Just hit this too :(

for (const [key, value] of Object.entries(obj)) { } is a very common code pattern that's really messed up by this.

I've resorted to disabling flow on affected lines :(

The reason this isn't supported was explained pretty simply by @vkurchatkin in my (now closed) PR:

Here is a simple example:

function test(val: { foo: string }) {
  Object.values(val).forEach(v => {
      v.toLowerCase();
  });
}

test({ foo: 'x', bar: 123 });

This going to pass typechecking, but fail at runtime

My PR would've set the following function type signatures for Object.entries, Object.values, and Object.keys, but was closed after it was pointed out how unsound these are:

-    static entries(object: any): Array<[string, mixed]>;
+    static entries <T> (o: { +[s: string]: T }) : Array<[string, T]>;
+    static entries <T> (o: Array<T> | $ReadOnlyArray<T>) : Array<[string, T]>;
+    static entries (o: any) : Array<[string, mixed]>;

+    static keys <T> (o: { +[key: T]: any }): Array<T>;
     static keys(o: any): Array<string>;

-    static values(object: any): Array<mixed>;
+    static values <T> (o: { +[s: string]: T }) : Array<T>;
+    static values <T> (o: Array<T> | $ReadOnlyArray<T>) : Array<T>;
+    static values(o: any): Array<mixed>;

@jcready that's not the case with exact types where we know exactly what the object contains. see my comment above https://github.com/facebook/flow/issues/2221#issuecomment-303359174

It's not been only once or twice when someone in our dev team has spent way too much time figuring out what's wrong with their flow typings when this has been the reason. supporting exact types would help a bit.

I think the above definitions are almost correct, but the object-as-map version should be replaced with this:

static values<T, O : { +[string] : T }>(val : $Exact<O>) : Array<T>;

(and likewise for keys and entries).

I'd love to provide a patch myself, but I currently don't have time to get the flow build working. I might give it a shot later this week, if no-one else took it before me.

EDIT: this definition is also incorrect, as @jcready pointed out

In a comment on another issue, @samwgoldman mentioned that support for better object-as-dictionary typings on exact objects was already his top priority. I think this issue might be closable since his work on that is already underway.

Let's close it when it's actually fixed :P

@villesau @TiddoLangerak the problem is that indexers in exact object types don't work as you'd expect (at least right now):

declare class Obj {
  static values <T> (o: {| +[s: string]: T |}) : Array<T>; // exact object w/ indexer
}

function test(val: {| foo: string |}) { // exact object w/o indexer
  Obj.values(val).forEach((v) => {
    v.toLowerCase();
  });
}

test({ foo: 'bar' });

This results in the following error:

6:   Obj.values(val).forEach((v) => {
                ^ property `foo`. Property not found in
2:   static values <T> (o: {| +[s: string]: T |}) : Array<T>;
                           ^ object type

Alternatively if we use @TiddoLangerak's definition for values:

static values<T, O : { +[string] : T }>(val : $Exact<O>) : Array<T>;

Then type-at-pos reports that the v parameter inside the forEach function is (unknown).

Unrelated, but @asolove I really wish the flow team would stop closing issues before they've actually fixed the issue. There seems to be an odd treatment of issues in this repo where they will be closed when the issue has simply been triaged or someone says they are going to make said issue a priority. Issues should be referenced in commit/PR messages so they can be closed automatically when the code actually gets merged to master.

You're right, I completely missed that v became untyped ๐Ÿ˜ž

Is anyone looking at resolving this?

Still facing the problem with 0.61.0

Actual in 0.63.1

@gabelevi: Any progress on this when working with exact types now that they are a thing? It is quite a ways since v0.32:

So Object.keys(obj) should return Array<'foo'> when obj has the type $Exact<{foo: string}>. This might make v0.31.0, but if it doesn't it will definitely make v0.32.0.

another repro https://flow.org/try/#0KYDwDg9gTgLgBDAnmYcCCAVZqC8cDeAUHHANoDWwiAXHAM4xQCWAdgOYC6tLArgLYAjYFFIcANIQC+AbkKFQkWHADGEFgxUBDADbKe2zTGAYIMHXDwAKOsG3BlRgCYBlYIbq1M2AJTd+QqAsAPgJiOChgGB4oFjgAeQEAK3sYADoANx0eYDprWxTgFzcYOm9UiMceZWBLS01lPT59Q2gxFWiIlhgANSzgb2DQkhJVdXgbNj5gLpMzbQt2qE6evvLCqpq8yemYNAb+ZphW+mBtrt7tbIGcEKJh4YiomJOz3f2mgyPAgGoXqfO+rJ7pI2gAGbyyMIkR7RWL1RqHaBwX4Tf4wWY6IFwEFwcGyGSEIA

Note that Object.keys works now, but Object.values and Object.entries are still broken. Trivial repro:

function test(obj: {|foo: string|}): Array<string> {
  return Object.values(obj); // still `mixed` and flow fails
}

https://flow.org/try/#0PQKgBAAgZgNg9gdzCYAoVUCuA7AxgFwEs5sx8BTAZ3wAo4AjAKwC4wBvAHyjjleoCdC2AOYcAvgEpWAQX78AhgE8APAKHCAfO1Rgw-cvkz9SAeSbkCAOgBu8mJip0mEgNyoxQA

Yeah, have same issue, but Object.keys(obj).map(key => obj[key]) solve problem

And you can trivially make that a bit more readable by making a flow-compatible helper function to replace Object.values with:

const values = <T>(obj: { [string]: T }): Array<T> =>
    Object.keys(obj).map(k => obj[k])

values({ "key1": "value1", "key2": "value2" }) // ["value1", "value2"]

Also faced this issue today using Object.entries and ended up going with Object.keys although @bdrobinson solutions is interesting but don't really want to add a helper function just for flow.

The Object.keys() approach is less performant as you're doing an extra key lookup against the object (obj[k]) (and instantiating the arrow function that's used in map()). I know it's a micro-optimisation, but I still think it's bad practice to mangle good code just to get Flow working.

Personally I think the right way to deal with this is to mark it with $FlowFixMe until Flow is flexible enough to mark it up without hacks (same as Symbol keys in objects, etc). Then just keep an eye on the changelog and see when these things get fixed.

const obj: { [string]: number } = { a: 1, b: 2, c: 3 };

// $FlowFixMe Object.values() returns mixed[] when it should return number[] https://github.com/facebook/flow/issues/2221
const arr: number[] = Object.values(obj);

I'm pretty sure there aren't major downsides to this approach (but would love feedback). I guess language things like this are the reason Flow isn't 1.0.0 yet and the team will get to it at some point down the line.

Only frustrating thing is how much time I waste hunting down a solution when I find one of these untypable situations ๐Ÿ˜ข Is there a good list of gotchas somewhere?

@dhoulb Flow is stable enough. Do not expect 1.0.0. Flow follow only major and patch from semver. Semver just can't fit to such projects because any feature is actually a breaking change.

@dhoulb : Since Object.values() returns an _array_, using Array<any> seems to fix the _error_.

const obj: { [string]: number } = { a: 1, b: 2, c: 3 };

const arr: Array<any> = Object.values(obj);

@gmaggio , but this way Flow will treat the elements of arr as any, instead of as number, right? So this doesn't solve the problem, just makes the error disappear.

@bdrobinson your values function is unsound. Here is a simple example:

const values = <T>(obj: { [string]: T }): Array<T> =>
  Object.keys(obj).map(k => obj[k]);

function test(val: { foo: string }) {
  values(val).forEach(v => {
    v.toLowerCase();
  });
}

test({ foo: 'x', bar: 123 });

This going to pass typechecking, but fail at runtime.

@jcready true, but the test function there means it's a slightly unfair example as you're mixing up using objects as maps and structs, which is an ambiguity in JS that isn't possible for flow to fully typecheck. Object.values is really only useful with objects being used as maps (indeed, that's what the OP is using this for) so I'd say it's down to the developer to not write functions like that.

This example without the naughty test function does correctly throw a typecheck error:

const values = <T>(obj: { [string]: T }): Array<T> =>
  Object.keys(obj).map(k => obj[k]);

values({ foo: 'x', bar: 123 }).forEach(val => {
    val.toLowerCase()
    //  Cannot call `val.toLowerCase` because property `toLowerCase` is missing in `Number`
});

If the declared type of the function argument passed to Object.values(obj).forEach was to be Any at the very least the programmer would be able to declare the expected type manually, thus propagating to the rest of the function, like Object.values(obj).forEach((value: MyType) => value.typeSpecificFunction()) without flow complaining about MyType being incompatible with mixed.
Inferring type is broken anyways in this case.

@gchamon that would be even less type-safe than the other proposed alternatives.

@sibelius Object.values is not special cased to handle $Exact object types. https://github.com/facebook/flow/blob/0b2dbd4fd1c1fa0e864b1f452f6ad99a490e6639/lib/core.js#L62

It would require flow to add a way to handle function overloading based on the exactness of a parameter such that you could say:

declare class Object {
  static values<T>(object: $SpecialExactObjectIdentifierWithValues<T>): Array<T>;
  static values(object: any): Array<mixed>;
}

Because you don't want to constrain any keys or values of the object. You'd just want to specify the exactness constraint.

The 2-year anniversary of this issue is approaching! ๐ŸŽ‰

Nobody uses Object.values at Facebook?

I also don't understand why they can't fix this issue. The behavior of the values and entries completely known, those isn't black boxes.

Relevant:

For those who are looking to use Object.values, here is a helper I am using:

type Map<T> = {
  [key: string]: T,
};

// Use native Object.values if it exists
// Otherwise use Object.keys
function values<T>(map: Map<T>): T[] {
  return Object.values
    ? // https://github.com/facebook/flow/issues/2221
      // $FlowFixMe - Object.values currently does not have good flow support
      Object.values(map)
    : Object.keys(map).map((key: string): T => map[key]);
}

Correct me if I'm wrong, but I believe this doesnt work if two attributes inside map has different types:

type foo = {
  id: number,
  name: string
} 

T must be number | string, and each value will have type T = number | string which is also not helpful.
But i agree, T is better than mixed.

aye, my example is for when it is a map of the same types

This along with #2174 (closely related) are in top10 issues when sorting by reaction. Flow is already amazing, but prioritizing this will take it even further!

A temporary solution to avoid flow errors:
You can cast 'mixed' to be a wanted type by casting it through 'any'.

type Line = { name: string };
type Table = { [id: string]: Line };
const table: Table = { 'random': { name: 'Test' } };

(Object.values(table): any).forEach((line: Line) => console.log(line.name)); // works

I follow @Frans-L example, but sometimes I don't want to import the Line type when I have a table value. I forcefully cast Array<Mixed> to Array<typeof table> and then silence the Flow error:

type Line = { name: string };
type Table = { [id: string]: Line };
const table: Table = { 'random': { name: 'Test' } };

// $FlowFixMe a meaningful description that mixed is being cast to `Table`.
(Object.values(table): Array<typeof table>).forEach((line) => console.log(line.name)); // works

This works plus it requires a useful comment left behind with why types were annotated manually.

@langri-sha If you like you can define a suppressed type that makes this a little more explicit and concise.

In .flowconfig:

# https://github.com/facebook/flow/issues/2221
suppress_type=$DownCast

Then in the code:

((Object.values(table): $DownCast): Array<Line>).forEach((line) => console.log(line.name)); // works

That way you don't have to create $FlowFixMe comments everywhere you use Object.values.

Another advantage is that this suppresses a specific error only, not all errors on that line. Your code had another error further on in the line that was also suppressed.

When you do this, whether you use my method or yours, you need to be very careful, as you're essentially telling Flow what the types are, and if you give the wrong type, it won't know. In your example code, typeof table is actually Table; Object.values on a Table won't return Array<Table>, but actually Array<Line>.

it means when you write Object.values(obj), we can't say with certainty what types are in the resulting array. sounds foolish because if obj is defined as

type AType = {|
  foo: string,
 bar: number
|}

const obj:{[string]: AType} 

you definitely must get an array of AType and not array of mixins.

I would just use actual es6 Map for maps

const map: Map<string, {|a: string|}> = new Map()

map.set('hello', {a: ''})

const values = Array.from(map.values())
;(values: Array<{|a: string|}>)

const keys = Array.from(map.keys())
;(keys: Array<string>)

https://flow.org/try/#0MYewdgzgLgBAtgQwA4C4YFlkB5oCcCWYA5gDQwDeAPgmnoUZQL4B8MAvDGAKYDuGyACgCUAKBGIkAOghcoAgOQALLgBsVIeWXI0Y8+Y1EjQkWADcEKgK5cI7GAEFcuBAE9JAM1wg4AiZPNWNsKiANwCAdYQaI7OLlhUOnTETMyGxtAwANZcLrYcMa4eXj5+2bnBImFlUQ5OrjhQBMSpQA

It makes sense to use ES6 Map objects for when you want a mutable map, rather than using objects for it. But I think it's fair to say that oldschool "objects-as-maps" serve some purpose still--for instance in APIs where an object is provided as a literal mapping keys to values, like combineReducers in Redux. Using plain objects for the state tree in Redux (and other similar cases) is another example.

The original issue is very old, and plenty of things have been added to Flow since... wouldn't it make for a modern libdef for Object.values to type it as something like <T: {}>(object: T) => Array<$Values<T>> (together with a declaration like the current one probably, to catch non-object cases)? After all, the documentation for $Values<T> states that it "represents the union type of all the value types of the enumerable properties in an Object Type T.", which I think is exactly what we want to model.

That said, the current implementation of $Values<T> seems pretty janky. I tried something like this:

declare class Object {
  static values<T: {}>(object: T): Array<$Values<T>>;
  static values(object: mixed): Array<mixed>;
}

declare var test: {
  name: string,
  value: number,
};

const v = Object.values(x);
// type-at-pos says v has type Array<$Values<{name: string, value: number}>>

const w: Array<string | number> = v;
// produces: Cannot assign v to w because:
//  โ€ข $Values [1] is incompatible with string [2] in array element.
//  โ€ข $Values [1] is incompatible with number [3] in array element.

Any eventual jankiness with $Values<T> should probably be sorted in a separate issue, but I hope we can improve the libdef for Object.values since it's rather hard to use at all currently... and I think it's important for the core libdef to model JS core as well as possible (whilst staying sound).

@FireyFly This isn't sound

class A {
  x: number;
  constructor(x: number) {
    this.x = x;
  }
  f() {
    return this.x;
  }
}

type O = {+x: number, +f: () => number};
const test: O = new A(2);

((2): $Values<O>); // own property
((() => 2): $Values<O>); // not own property

console.log(Object.values(test)); // [2]

Oh hmm... right, that's a good point, my bad. I guess the iterating over all properties rather than only own properties means we can't really do any better for a type for Object.values then. :

For sealed objects, which are very-well inferable, values/keys/entries should return sealed arrays, which also seem inferable.

Nice. Was promised a better way and burden-free development, but seems like another day with more problems of trying to incorporate the Flow into a new project.

Is anyone actually looking at this to provide some solutions or even, dare to say it, fix this? Today is the third anniversary of this issue.

@laszukdawid yes, @goodmind has made sound versions of values/entries, but apparently this still isn't good enough for the flow maintainers.

@jcready That's not a very generous interpretation. Why not jump on discord and discuss it with them?

@lyleunderwood what would you consider a more generous interpretation? I asked if I should make a PR for @goodmind's proposed libdef changes. @goodmind said:

@dsainati1 doesn't like idea of exact constraint.

[...] this was from Discord months ago when I proposed this libdef change

I attempted to find the conversation on discord and I believe this is it. @dsainati1's argument against it was:

the whole concept of a supertype of all exact objects is inherently flawed though, since the point of exact objects is that they don't exhibit width subtyping

My interpretation of this is basically that even though @goodmind's implementation using an exact constraint is sound in practice (or at least as sound as the current Object.keys() implementation), in theory it just doesn't _feel_ right to @dsainati1.

The most reasonable argument against the exact constraint implementation I've see was from @samwgoldman in this comment from January 2018:

Vladimir is right. There are two issues.

Width subtyping:

var o: { p: number, q: string } = { p: 0, q: "", foo: "foo" };
var keys: "p" | "q" = Object.keys(o); // lies
var vals: number | string = Object.values(o); // lies

Proto vs. own

var o: { p: number, q: string } = Object.create({ p: 0, q: "" });
var keys: Array<"p" | "q"> = Object.keys(o); // lies
var vals: Array<number | string)> = Object.values(o); // lies

Exact object types are great for the former case, because they by definition exclude values having more properties. For the latter case, we really need to provide better mechanisms to specify object layout as part of types, since so many APIs depend on it. It's on our radar.

And while I agree that the proto vs. own issue still isn't solved by @goodmind's exact constraint implementation, the fact is flow has this same issue with Object.keys() right now. I'll quote myself here:

So if this concession is made for Object.keys(), why not also for Object.values() or Object.entries()?

The impression I got was that flow maintainers didn't want to make an API knowing they'll just have to change the API again down the road when the theoretically correct solution eventually comes along.

For what its worth, if I could override the builtin libdef with @goodmind's exact constraint, I would have by now.

Here's a workaround that uses Object.keys() instead of Object.values():

type Line = { name: string };
type Table = { [id: string]: Line };
const table: Table = { 'random': { name: 'Test' } };

// Error.
Object.values(table).forEach((line: Line) => console.log(line.name));
// No error.
Object.keys(table).map(key => table[key])
  .forEach((line: Line) => console.log(line.name));

Try it out on Flow playground

The object.keys workaround is an old a know one, but I think that not using some of the best ES6 features because flow it's a bad thing to face.
I think this should be prioritised since it has been open for more than 3 years.

+1 on this issue. Still using the workaround.

Closing in on 4 years on this issue. Any updates?

It really is an inconvenience when you spend 2 minutes writing a reduce function on Object.values() and then another hour on trying to write it in a way that flow would allow when you know exactly what .values() should return. On top of that everyone on your team will ask for an explanation why you had to write it the way you did (see all the workaround ideas above), because it is counter-intuitive/ looks like bad practice (e.g. the any solution).

Hey guys!

Is cast to : any really our best solution?

Can we do better than that? :smile:

๐ŸŽ‰ Happy Birthday!! ๐Ÿฅณ๐ŸŽ‚

Check for the type of the Object. In my case I accidentally set the type of the variable as Array<Object> but instead it should just be an Object.

If there's any condition associated inside the map function, we need to return some JSX for the callback or else null.

It's always advisable to use a variable outside the parent return to capture the results from a function (return value of Object.keys((callback))) and then use that variable to render inside the parent JSX to avoid flow from catching issues like:

  • Array.prototype.map() expects a value to be returned at the end of arrow function
  • Expected to return a value at the end of arrow function.
Was this page helpful?
0 / 5 - 0 ratings