Flow: Allow string literal type alias in $PropertyType

Created on 21 Aug 2016  路  9Comments  路  Source: facebook/flow

/* @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

Most helpful comment

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;
}

All 9 comments

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>;

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

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);

try

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

Was this page helpful?
0 / 5 - 0 ratings