Flow: Object.values() doesn't take into account indexed properties : no inference + type error

Created on 1 Sep 2017  路  12Comments  路  Source: facebook/flow

鉁忥笍 EDIT: It seems to be fixable like that

function getValues <Obj:Object>(o: Obj) : Array<$Values<Obj>> {
    return Object.values(o)
}

see this comment: https://github.com/facebook/flow/issues/4771#issuecomment-326571246


https://flow.org/try/#0PQKgBAAgZgNg9gdzCYAoVAXAngBwKZgCiAHgIYC2OMBAvGAN4DaA1nlgFwB2AruQEZ4ATgF0w7AM4ZBAS04BzAL6pqGMHD4ArMUTKVqYOvSXqNjAIyi6AIjjMwZq8ryqAbqRjjtAQUGDSWAB5JGXkAPgMwAHlNPABjDAA6NxhuPHEAChMASlQgA

/* @flow */

type Example = {[key:number] :string}
let obj : Example = {}
obj[1] = "ok 1"
let vals : Array<string> = Object.values(obj)

馃敶 gives

6: let vals : Array<string> = Object.values(obj)
                    ^ string. This type is incompatible with
[LIB] static/v0.54.0/flowlib/core.js:52:     static values(object: any): Array<mixed>;
                                                                               ^ mixed
object model

Most helpful comment

full example:

function getValues <Obj:Object>(o: Obj) : Array<$Values<Obj>> {
    return Object.values(o)
}

type M1 = {[a:number]: string}
type M2 = {[a:number]: number}

let v1ok : Array<$Values<M1>> = ["33","44","55"]
let v2ok : Array<$Values<M2>> = [12,22]

let v1 : Array<$Values<M1>> = ["33",22]
//                                  ~~ error
let v2 : Array<$Values<M2>> = ["33",22]
//                             ~~~~ error

let t1 : Array<string> = getValues({a: 'ok', b:'stuff'})

let t2 : Array<string> = getValues({a: 'ok', b:44})
//                                             ~~ error

All 12 comments

does anyone know a workaround ?
ES6 Maps or ImmutableJs are unfortunately not an option for me.

You can do this:

let vals : Array<string> = (Object.values(obj): any)

Objects can't have numbers as keys. If you try to set a numeric key on an object it will automatically be cast to a string:

let obj = {}
obj[1] = "ok 1"
JSON.stringify(obj)
// {"1":"ok 1"}

Unfortunately it would be unsound to define Object.values/entries/keys any other way. I previously made a PR to do what you want, but I had to close it after it was pointed out how unsound the changes were:

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

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

@jcready actually, thanks to $Value, it is possible:

function getValues <Obj:Object>(o: Obj) : Array<$Values<Obj>> {
    return Object.values(o)
}

type M1 = {[a:number]: string}
type M2 = {[a:number]: number}

let v1ok : Array<$Values<M1>> = ["33","44","55"]
let v2ok : Array<$Values<M2>> = [12,22]

let v1 : Array<$Values<M1>> = ["33",22]
//                                  ~~ error
let v2 : Array<$Values<M2>> = ["33",22]
//                             ~~~~ error

this works fine for me

I learnt about $Values 5 minutes ago reading this comment: https://github.com/facebook/flow/issues/2174#issuecomment-316739515

sadly, it is not documented here: https://flow.org/en/docs/types/utilities/

full example:

function getValues <Obj:Object>(o: Obj) : Array<$Values<Obj>> {
    return Object.values(o)
}

type M1 = {[a:number]: string}
type M2 = {[a:number]: number}

let v1ok : Array<$Values<M1>> = ["33","44","55"]
let v2ok : Array<$Values<M2>> = [12,22]

let v1 : Array<$Values<M1>> = ["33",22]
//                                  ~~ error
let v2 : Array<$Values<M2>> = ["33",22]
//                             ~~~~ error

let t1 : Array<string> = getValues({a: 'ok', b:'stuff'})

let t2 : Array<string> = getValues({a: 'ok', b:44})
//                                             ~~ error

It's still unsound to define Object.values using $Values. This still passes type checking, but will throw a runtime error:

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

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

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

The only way it would be safe to define Object.values using $Values is if flow provided some way to only let $Exact objects flow in. Something like this hypothetical syntax:

declare class Object {
  static values <T: $ExactObject> (obj: T) : Array<$Values<T>>;
  static values (obj: any) : Array<mixed>;

  static keys <T: $ExactObject> (obj: T) : Array<$Keys<T>>;
  static keys (obj: any) : Array<string>;

  static entries <T: $ExactObject> (obj: T) : Array<[$Keys<T>, $Values<T>]>;
  static entries (obj: any) : Array<[string, mixed]>;
}

Then my test function would fail type checking because I've annotated my input parameter val as an inexact object type, so flow would use my secondary Object.values definition which returns Array<mixed>. And if I redefined my test function to use an exact object type for its input parameter val (e.g. {| foo: string |}) then my invocation of test({ foo: 'x', bar: 123 }) would fail type checking because the object I passed it doesn't match the exact object type.

@jcready thanks for your example

It's still unsound to define Object.values using $Values.

It depends. it is "sound" in my codebase, because I have other ways to check and maintain my invariants. In any way, it seems to me that it is the best possible way so far to do what I want.

Other solutions like this are much _less_ safe :

let vals : Array<string> = (Object.values(obj): any)

This being said, I agree that changing the core Object.values type definition is probably not a good idea. On the other side, defining a getValues function as shown above may bring more type safety in many cases.

馃摑 Also, most people who try to do that probably know that there is some invariant in their codebase that should allow them to do that.

At least it sounds with maps:

export const values: <T>({+[string]: T}) => T[] = (Object.values: $FlowIssue);
export const entries: <T>({+[string]: T}) => [string, T][] = (Object.entries: $FlowIssue);

I had luck with implementing the $ObjMap

Was this page helpful?
0 / 5 - 0 ratings

Related issues

damncabbage picture damncabbage  路  3Comments

pelotom picture pelotom  路  3Comments

jamiebuilds picture jamiebuilds  路  3Comments

bennoleslie picture bennoleslie  路  3Comments

cubika picture cubika  路  3Comments