Flow: Use type from field of object type

Created on 8 Jul 2016  路  23Comments  路  Source: facebook/flow

If I have this object type:

type Person = {
  name: string
  age: number
}

There exists an undocumented $Keys feature for specifying a "key set" type (the key must be one of the keys of the object). It would be neat to also somehow enforce that a type must be a type of a specific field of the object, like

function get(person, field: $Keys<Person>) : $Value<Person(arg:field)> {
  return person[field];
}

I'm very new to flow, and I have no good proposal for the syntax. Writing this out, it is kind of difficult because you need to parameterize the field lookup with an argument value (given that it's a literal and flow can analyze it, just like it does with $Keys).

This came out of trying to type immutable.js. One can specify a type for Record:

export type Record<T: Object> = {
  get<A>(key: $Keys<T>): A;
  set<A>(key: $Keys<T>, value: A): Record<T>;
  setIn(keyPath: Array<any>, ...iterables: Array<any>): Record<T>;
  update<A>(key: $Keys<T>, updater: (value: A) => A): Record<T>;
  merge(values: $Shape<T>): Record<T>;
  mergeIn(keyPath: Array<any>, ...iterables: Array<any>): Record<T>;
  inspect(): string;
  toObject(): Object & T;
} & T;

And accesses like foo.x will be typed (because of the intersection type) but foo.get("x") will not be, even though Flow _does_ enforce that x is a valid key of the foo record.

Most helpful comment

UPDATE:

This partially works: UPDATED

/* eslint-disable */
// @flow
type $ObjectPair<K, V> = {[key: K]: V} | Object
type _$Value<Key: string, V, O: $ObjectPair<Key, V>> = V
type $Value<O: Object, K: $Keys<O>> = _$Value<K, *, O>

type Obj = {
  c: number,
  b: boolean,
  a: string,
}

declare var y: $Value<Obj, 'b'>
;(y: string)
;(y: boolean) // Should Show an Error Here, But Doesn't
;(y: number) // Should Show an Error Here, But Doesn't

declare var x: $Value<Obj, 'a'>
;(x: number) // Correctly Shows an Error Here
;(x: boolean) // Correctly Shows an Error Here
;(x: string)

declare var z: $Value<Obj, 'c'>
;(z: string) // Should Show an Error Here, But Doesn't
;(z: boolean)
;(z: number) // Should Show an Error Here, But Doesn't

Some other information:

  • If I try to use & instead of | for $ObjectPair Everything breaks down. Though, I think that would be more correct.
  • Even if I change the order of the keys in the Obj type, nothing changes.
  • If I delete, the key a everywhere, the types work correctly for 'b' but nothing else. So it looks like this only works for alphabetically the first key in the object.
  • If I change $Keys<O> to just string, everything remains the same, except the type starts accepting non-keys.

Anyone have any ideas? @jeffmo @samwgoldman

All 23 comments

cc @bhosmer (per https://twitter.com/mroch/status/751496819327143936 )

Another issue with typing immutable.js (which is probably unrelated, but worth noting anyway) is setIn and friends. They take a path, obj.setIn(["foo", "bar"], "value") and it's impossible to type that, even though we have all the structure of foo and bar fields we need. I have no idea how that would even work, but it would be neat if it did.

EDIT: to clarify, the end goal would be that if "obj.foo.bar" should be a number, the above expression would throw a type error.

@jlongster here is an idea that might help: http://stackoverflow.com/questions/37952020/is-it-possible-to-wrap-a-flow-type-in-an-immutable-container. It's pretty verbose, but should work

Syntax that makes sense could be like this:

function get<K: $Keys<T>>(field: K) : $Value<T, K> {
  return person[field];
}

@vkurchatkin Can't quite tell what that stack overflow issue is about, but it almost sounds like they want maps to returns different values for separate get calls. That's not really what I want. If I have nested data structures, I just want obj.getIn(["foo", "bar"]) to work, just like obj.foo.bar would be typed.

I like that syntax a lot.

I just want obj.getIn(["foo", "bar"])

I think this pattern is just too complicated, unfortunately.

I like that syntax too. I'm not sure what the flow team's opinion on these magic types is, but this would be awesome. And it's also solve another problem I've been unable to solve so far, which convert one object type into another object type, basically by 'mapping' over the value types. Assuming this monstrosity works!

(P.S. checked my monster types over in try flow, and it totally works!)

import {Record} from 'typed-immutable';

type $Function1<A, B> =  (arg: A) => B;
type _Function1Value<A, B, F: $Function1<A, B>> = B;
type $Function1Value<F: $Function1<*, *>, A> = _Function1Value<A, *, F>;

type RecordType = <T: Object>(obj: T) => Class<{
  [key: $Keys<T>]: $Function1Value<$Value<T, key>, any>
}>;

// now:
var record = new Record({a: Number, b: String, c: (x) => String(x)});

(record: {
  a: number,
  b: string,
  c: string
});

This means, if we can get the $Value magic type, I can finally have a perfectly working type definition for react-redux connect function.

UPDATE:

This partially works: UPDATED

/* eslint-disable */
// @flow
type $ObjectPair<K, V> = {[key: K]: V} | Object
type _$Value<Key: string, V, O: $ObjectPair<Key, V>> = V
type $Value<O: Object, K: $Keys<O>> = _$Value<K, *, O>

type Obj = {
  c: number,
  b: boolean,
  a: string,
}

declare var y: $Value<Obj, 'b'>
;(y: string)
;(y: boolean) // Should Show an Error Here, But Doesn't
;(y: number) // Should Show an Error Here, But Doesn't

declare var x: $Value<Obj, 'a'>
;(x: number) // Correctly Shows an Error Here
;(x: boolean) // Correctly Shows an Error Here
;(x: string)

declare var z: $Value<Obj, 'c'>
;(z: string) // Should Show an Error Here, But Doesn't
;(z: boolean)
;(z: number) // Should Show an Error Here, But Doesn't

Some other information:

  • If I try to use & instead of | for $ObjectPair Everything breaks down. Though, I think that would be more correct.
  • Even if I change the order of the keys in the Obj type, nothing changes.
  • If I delete, the key a everywhere, the types work correctly for 'b' but nothing else. So it looks like this only works for alphabetically the first key in the object.
  • If I change $Keys<O> to just string, everything remains the same, except the type starts accepting non-keys.

Anyone have any ideas? @jeffmo @samwgoldman

@nmn did you ever get any further on this?

@skevy think we caught up on twitter.

But the best I could do was get a $values function that will give the union of the types of all values of an object.

But it's not going to ever be an enum.

There needs to be a way to tell flow that something is a literal for anything else.

Typescript introduced this feature today in 2.1 as "indexed access types" or "lookup types." This is an incredibly useful feature and I really think Flow would benefit from its introduction.

This is what $PropertyType<V, K> does, but it does not (yet) take arbitrary types.

type Obj = {
  a: string,
  b: boolean,
  c: number,
};

declare var x: $PropertyType<Obj, 'a'>;
(x: string); 
(x: number);   // ExpectError
(x: boolean);  // ExpectError

declare var y: $PropertyType<Obj, 'b'>;
(y: number);   // ExpectError
(y: string);   // ExpectError
(y: boolean);  

declare var z: $PropertyType<Obj, 'c'>;
(z: number); 
(z: string);   // ExpectError
(z: boolean);  // ExpectError

Try Flow

An example of the getter syntax:

type Person = {
  name: string,
  age: number,
};

function get<T: $Keys<Person>>(person, field: T): $PropertyType<Person, T> {
  return person[field];
}

const person: Person = {name: 'Sam', age: 27};
const a:string = get(person, 'name');

This unfortunately throws:

6: function get<T: $Keys<Person>>(person, field: T): $PropertyType<Person, T> {
                                                     ^ expected object type and string literal as arguments to $PropertyType

which will hopefully be fixed with https://github.com/facebook/flow/pull/2952 if it is finished.

You can use $ElementType for this 馃憤

type Person = {
  name: string,
  age: number,
};

declare function get<K: $Keys<Person>>(person: Person, field: K): $ElementType<Person, K>;

(get((null: any), 'name'): number);

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

Why does both $PropertyType and $ElementType exist?

Historical reasons. I would like to introduce O[P] syntax at some point and we鈥檒l remove $PropertyType and $ElementType then.

@STRML
I've been trying to replicate what you are doing here (https://github.com/facebook/flow/issues/2057#issuecomment-291637345) but cannot get this to work on an imported type.

// types.js
export type Card = {
  id: string,
};

declare var x: $PropertyType<Card, "id">;
(x: boolean);  // [flow] string (This type is incompatible with boolean)

// someOtherFile.js
// Example 1
import { type Card } from "../../types";

declare var x: $PropertyType<Card, "id">;
(x: boolean); // (parameter) x: boolean (no error)
// someOtherFile.js
//  Example 2 
import { type Card } from "../../types";

type Props = {
  id: $PropertyType<Card, "id"> // flow thinks tells me id is of type any
};

// if I try
type Props2 = {
  id: $PropertyType<Card, "someRandomProperty"> // no error
};

Am I doing something wrong here? I'm trying to push my understanding of flow. Is the utility type $PropertyType supposed to be used this way?
Ultimately what I am trying to do is say:

  • Here is a type Card. It contains a number of properties.
  • I have a component that takes in all properties of Card
  • I have a subcomponent that takes in only some properties of Card passed down from the parent component
  • I don't want to type those properties again, instead I'd like to say, these properties are some of the properties typed in type Card.

If this is the wrong place to ask, please let me know.

@calebmer Thanks for your example, it works perfectly for the type definition.

I'm finding troubles while implementing a set function though, due to the dynamism of the object index operator:

type Person = {
  name: string,
  age: number,
};

function set<K: $Keys<Person>>(person: Person, field: K, value: $ElementType<Person, K>): Person {
  person[field] = value; // Cannot assign `value` to `person[field]`
  return person;
};

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

I can't find a way to type your set function for now, but maybe this workaround will be helpful.

I'm just starting to grasp the advanced Flow usages, was playing around with this code but getting errors:

type Column<T> = {
    key: $Keys<T>,
    render: (value: $ElementType<T, $PropertyType<Column<T>, 'key'>>) => string,
}

function renderColumn<T: Object>(obj: T, column: Column<T>): string {
    return column.render(obj[column.key])
}

renderColumn({
    foo: 'string',
    bar: 1,
}, {
    key: 'foo',
    render: (foo) => foo, // simplified, could return a React node
})

What I will probably do is add a static type parameter like so:

type Column<T, K: $Keys<T>> = {
    key: K,
    render: (value: $ElementType<T, K>) => string,
}
function renderColumn<T: Object, K: $Keys<T>>(obj: T, column: Column<T, K>): string {
    return column.render(obj[column.key])
}
const column: Column<Data, 'foo'> = { key: 'foo', render: (foo) => foo }
renderColumn({ foo: 'string', bar: 1 }, column)

Fyi we figured out how to use this for concrete types. Here's a flow try

@danielbartsch Nice trick, I wouldn't have thought to intersect with +{[string]: mixed}. I wonder why this works.

got it from the flow docs of $ElementType https://flow.org/en/docs/types/utilities/#toc-elementtype

Was this page helpful?
0 / 5 - 0 ratings

Related issues

opensrcery picture opensrcery  路  88Comments

sebmck picture sebmck  路  113Comments

gabro picture gabro  路  57Comments

Gozala picture Gozala  路  54Comments

Macil picture Macil  路  47Comments