Allow string literal type alias in $PropertyType
samwgoldman opened this issue ยท 9 comments
/* @flow */
type Key = 'key';
type T = { key: number };
declare var prop: $PropertyType<T,Key>;
Actual output:
4: declare var prop: $PropertyType<T,Key>;
^ expected object type and string literal as arguments to $PropertyType
Expected: No error
I'm sure that this is a really hard problem, but I wonder if $PropertyType
could be extended to work in this case: (based on Immutable.js)
declare class Record<T: Object> {
static <T: Object>(spec: T, name?: string): Class<T & Record<T>>;
get<K: $Keys<T>>(key: K): $PropertyType<T, K>;
set<A>(key: $Keys<T>, value: A): T & Record<T>;
remove(key: $Keys<T>): T & Record<T>;
}
Currently, Flow complains that $PropertyType
only a string literal as the second parameter. This one feature (if possible) would make it possible to write the type for every thing that it's currently impossible to write types for.
I see that a problem is that K is currently a subtype of a string Union, but it could still be a union itself.
If Flow also had $StringLiteral
type (issue exists), this would be more sound:
get<K: $Keys<T> & $StringLiteral>(key: K): $PropertyType<T, K>;
I've tried to make this work in User Space, but it hasn't worked. The closest I've got is to get the union of all value types in an Object. So $Keys
but for values:
type $Object<V> = {[key: string]: V}
type _$Values<V, O: $Object<V>> = V
type $Values<O: Object> = _$Values<*, O>
Using this in the Immutable.js Record type:
declare class Record<T: Object> {
static <T: Object>(spec: T, name?: string): Class<T & Record<T>>;
construtor(spec: T): this;
get(key: $Keys<T>): $Values<T>;
set<A>(key: $Keys<T>, value: A): T & Record<T>;
remove(key: $Keys<T>): T & Record<T>;
}
This gives us slightly better results.
import {Record} from 'immutable'
type P = {
name: string,
age: string
}
const Person = Record({name: 'John Doe', age: '25'})
var bob: Record<P> = new Person({name: 'Bob', age: '31'})
;(bob.get('name'): string) // no error here, but flow doesn't infer the type automatically.
// manual typecasting is still required
;(bob.get('name'): number) // Flow Error: it's a string not a number
@samwgoldman would allowing a string literal type also allow what @nmn is asking for? Being able to do $PropertyType<T, K>
in the general case would be really powerful.
So are there any plans to introduce $Values utility type?
Which takes object type as parameter and gives a union of values.
as a point of reference, Typescript 2.1 introduced the ability to type set
functions via K keyof T
and T[K]
. From their release notes:
function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]) {
obj[key] = value;
}
You can use $ElementType
for this! If you are having any issues please let us know ๐
type Key = 'key';
type T = { key: number };
declare var prop: $ElementType<T,Key>;
This looks very promising, @calebmer. However, the following code doesn't typecheck for me:
export type SomeType = {
aString: string,
aNumber: number,
};
function f2<S: $Keys<SomeType>>(
propertyName: S,
value: $ElementType<SomeType, S>,
) {
};
function f1<S: $Keys<SomeType>>(
propertyName: S,
value: $ElementType<SomeType, S>,
) {
f2(propertyName, value);
};
f1("aNumber", 2);
If f1
doesn't call f2
it typechecks. Am I missing something here?
$ElementType Doesn't type-check asd I would expect when used as a return-value:
Type checking fails in Foo.getValue(), arguing that the return type of ValueOf is incompatible with each of "number", "string", and "object type" in turn.
type T = {|
key: number,
foo: string,
bar: {}
|};
type ValueOf<K: $Keys<T>> = $ElementType<T, K>;
class Foo {
obj: T;
getValue<K: $Keys<T>>(key: K): ValueOf<K> {
return this.obj[key];
}
}
15: return this.obj[key];
^ number. This type is incompatible with the expected return type of
14: getValue<K: $Keys<T>>(key: K): ValueOf<K> {
^ object type
15: return this.obj[key];
^ number. This type is incompatible with the expected return type of
14: getValue<K: $Keys<T>>(key: K): ValueOf<K> {
^ string
15: return this.obj[key];
^ object type. This type is incompatible with the expected return type of
14: getValue<K: $Keys<T>>(key: K): ValueOf<K> {
^ number
15: return this.obj[key];
^ object type. This type is incompatible with the expected return type of
14: getValue<K: $Keys<T>>(key: K): ValueOf<K> {
^ string
15: return this.obj[key];
^ string. This type is incompatible with the expected return type of
14: getValue<K: $Keys<T>>(key: K): ValueOf<K> {
^ number
15: return this.obj[key];
^ string. This type is incompatible with the expected return type of
14: getValue<K: $Keys<T>>(key: K): ValueOf<K> {
^ object type
A work-around is to use $Subtype
type T = {|
key: number,
foo: string,
bar: {}
|};
type ValueOf<K: $Keys<T>> = $ElementType<T, K>;
class Foo {
obj: T;
constructor(values: T) {
this.obj = values;
}
setValue<K: $Keys<T>>(key: K, value: ValueOf<K>) {
return this.obj[key];
}
getValue<K: $Subtype<$Keys<T>>>(key: K): $ElementType<T, K> {
return (this.obj[key]: ValueOf<K>);
}
}
// Works
const x = new Foo({"key": 1, "foo": "bar", "bar": {}});
x.setValue("key", 2);
const y: number = x.getValue("key");
// Correctly fails to type check
x.setValue("key", {});
// Correctly fails to type check
const z: number = x.getValue("foo");
@calebmer can you elaborate what are the differences between $PropertyType and $ElementType? My little test says there are no differences: https://flow.org/try/#0C4TwDgpgBA8gRgKygXigbwFBSmATgezAC50A7AVwFsSLK4JcBfDZjACjVpIEYAmAZkYkAJAFEANhEoRSwACrgIAHngIANFADkeQpoB8ASgDc7TlR4ChUYQAUCkXKAWQViDdvv7jQA