Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow string literal type alias in $PropertyType #2310

Closed
samwgoldman opened this issue Aug 21, 2016 · 9 comments
Closed

Allow string literal type alias in $PropertyType #2310

samwgoldman opened this issue Aug 21, 2016 · 9 comments

Comments

@samwgoldman
Copy link
Member

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

@nmn
Copy link
Contributor

nmn commented Sep 11, 2016

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

@nmn
Copy link
Contributor

nmn commented Sep 11, 2016

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

@aaronjensen
Copy link

@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.

@yarax
Copy link

yarax commented Jan 9, 2017

So are there any plans to introduce $Values utility type?
Which takes object type as parameter and gives a union of values.

@bywo
Copy link

bywo commented Feb 8, 2017

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

@calebmer
Copy link
Contributor

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

@philippotto
Copy link

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?

@emdash
Copy link

emdash commented Aug 17, 2017

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

@villesau
Copy link
Contributor

villesau commented Dec 5, 2017

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants