facebook / flow

Adds static typing to JavaScript to improve developer productivity and code quality.
https://flow.org/
MIT License
22.08k stars 1.86k forks source link

No way to access the literal type at a specific object key? #6651

Open callumlocke opened 6 years ago

callumlocke commented 6 years ago

I'm trying to access the literal type of the value at a specific key of a given object, in order to use with generics to describe the complex relationship between a function's input and return type.

If you freeze an object, you can use the $Values utility to get a union of literal types based on an object's values:

const foo = Object.freeze({
  a: 'bar',
  b: 123,
  c: true,
})

type MyType = $Values<typeof foo> // 'bar' | 123 | true

('bar': MyType)  // ok
(123: MyType)    // ok
('nope': MyType) // flow error

That works fine. But what if I want to get only one literal type, at a specific key, foo.a (i.e. 'bar' in this case)? I've tried several ways but they all lose accuracy, becoming the primitive string type:

type Attempt1 = typeof foo.a // string

type Attempt2 = $PropertyType<typeof foo, 'a'> // string

type Attempt3 = $ElementType<typeof foo, 'a'> // string

type Attempt4 = $Values<
  $Diff<
    typeof foo,
    $Diff<typeof foo, { a: string }>
  >
> // string

Is there any way to do it?

fishythefish commented 6 years ago

You can annotate the type of foo or make the type of foo.a explicit: a: ('bar': 'bar').

callumlocke commented 6 years ago

@fishythefish thanks but I need a way to do it dynamically for an object that is not known in advance, accessed only via via a generic reference.

bradennapier commented 6 years ago

Yeah this would appear to be a bug. Most likely when they allowed the unions via Object.freeze they did not extenf the interface to handling other type calls like $PropertyType, etc.

In this case, Flow should absolutely know it will be 'bar' and never anything else because it is no longer possible for foo.a to be any other value than 'bar'.

The bugs with losing type safety for what appears to be no reason really are fairly annoying.

There is absolutely no valid reason that this should be required when Object.freeze is used:

const foo = Object.freeze({
  a: ('bar': 'bar'),
  b: (123: 123),
  c: (true: true),
});

I think the main issue is that Flow is assuming that Object.freeze results in {| a: string, b: number, c: boolean |} when it should be transforming it into {| +a: 'bar', +b: 123, +c: true |}