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.
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.
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:
&
instead of |
for $ObjectPair
Everything breaks down. Though, I think that would be more correct.Obj
type, nothing changes.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.$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
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);
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:
Card
. It contains a number of properties.Card
Card
passed down from the parent componenttype 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;
};
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
Most helpful comment
UPDATE:
This partially works: UPDATED
Some other information:
&
instead of|
for$ObjectPair
Everything breaks down. Though, I think that would be more correct.Obj
type, nothing changes.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.$Keys<O>
to just string, everything remains the same, except the type starts accepting non-keys.Anyone have any ideas? @jeffmo @samwgoldman