Flow: Union from array literal

Created on 20 Oct 2015  Â·  27Comments  Â·  Source: facebook/flow

Let's say a value can be one of a finite number of string values:

type State = 'disconnected' | 'connecting' | 'connected';
let state: State = ...;

If I already have an array of all valid states in my code somewhere, it'd be nice if it could be used to create a type union from that, e.g.:

const STATES = ['disconnected', 'connecting', 'connected'];
type State = $Values<STATES>;
let state: State = ...;

I'm proposing $Values here analogously to $Keys. Alternatively, $Either could be extended to not just accept a list of types but also an array.

feature request

Most helpful comment

I'm not so sure: $Values can't work with arrays and the title of this issue is "Union from array literal"

All 27 comments

Is there any way to do this yet? I want to define a type where the values in one argument, an array, are passed in as keys of an object in a function later down.

Bump. This would work well with the proposed $Values.

Will this thing happen at any point? Been rotting for a while and similar requests are all around the flow issues

@calebmer landed ab0789a today, which I believe should make it out with the next release of flow. I know I personally can't wait to use it 🎉

thanks @calebmer!

Interestingly, there are no tests for $Values<T> on an Array?

Interestingly, there are no tests for $Values on an Array?

Because it doesn't work with arrays, only with objects

Is that planned?

On Jun 20, 2017 12:05 AM, "Vladimir Kurchatkin" notifications@github.com
wrote:

Interestingly, there are no tests for $Values on an Array?

Because it doesn't work with arrays, only with objects

—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/facebook/flow/issues/961#issuecomment-309645745, or mute
the thread
https://github.com/notifications/unsubscribe-auth/ABJFP23DwjhhsgFbvM0Oe-D1N34ENylcks5sF1MHgaJpZM4GSaOq
.

Here is a simple example of how $Values works now:

const MyEnum = {
  foo: 'foo',
  bar: 'bar'
};

type MyEnumT = $Values<typeof MyEnum>;

('baz': MyEnumT); // No error

For the same reason even if it worked with arrays, it wouldn't work the way you want.

Assumedly that's because the type of:

const Suite = {
  DIAMONDS: 'Diamonds',
  CLUBS: 'Clubs',
  HEARTS: 'Hearts',
  SPADES: 'Spades',
}

is not what you might think, it's:

{
  DIAMONDS: string,
  CLUBS: string,
  HEARTS: string,
  SPADES: string,
}

which speaks to the need for some kind of helper to get the actual values of an object when using typeof, rather than the types of those values.

Yeah I'm not sure what the current $Values implementation gives us. It works well for the way Facebook likes to define enums through alias objects, but I don't understand how, for instance, you can assert that a given string value is in fact a valid member of an enum.

For instance, imagine you're reading a value from a file. Using my original example, I'd like to see something as succinct as this:

const STATES = ['disconnected', 'connecting', 'connected'];
type State = $Values<STATES>;

function readState(filename: string): State {
  const state = fs.readFileSync(filename); // `state` is just `string` for now
  invariant(STATES.includes(state)) // `state` is now proven to be of State
  return state;
}

I've filed this separately in #4454.

@philikon But that's because they're actually defining the type's values directly. See this example, which throws no errors, for what happens if you don't.

@STRML ah yes you're right. I guess that's due to #2639. What a mess.

Does this also cover the case of dynamically building a union type?

Example:

// A list of existing flow types
const accountTypes = [CheckingAccount, BankingAccount, InvestmentAccount];
// What I have to do now
export type Account = CheckingAccount | BankingAccount | InvesmentAccount;

// My software has a plugin system, so any time a new plugin is added,
// I have to go update this list as well.

// What would be nice is some way to build a union type dynamically from an array
export type Account = accountTypes.reduce((flowUnion, accountType) => (
  flowUnion.add(accountType);
), new FlowUnion());

// Or...
export type Account = $Union<accountTypes>;

I think that's what this ticket is requesting, right? It's just calling it $Values. Or am I mistaken?

This issue should probably be closed now that $Values has been shipping for a while.

// Note: must use Object.freeze for Flow to know the value will not change.
const MyObj = Object.freeze({
  DIAMONDS: 1,
  CLUBS: 2,
  HEARTS: 3,
  SPADES: 4,
})

type MyObjType = $Values<typeof MyObj>
// No Error
const testMyObjEntry1: MyObjType = 3
// Expect Error
const testMyObjEntry2: MyObjType = 5

type MyObjKey = $Keys<typeof MyObj>
// No Error
const testMyObjKey1: MyObjKey = 'HEARTS'
// Expect Error
const testMyObjKey2: MyObjKey = 'JOKERS'

https://flow.org/try/#0PTAEDkHsBcFMC5QFsCuBnap21AeQEYBWsAxtAHQBmATrLAF46WTWgBiANpAO6jSSgA1gDsefABY4AbgEMOKHNwCWHDqFGYS4mcIDmscgCgSkYRlABZAJ4FCoALx4ipCjTqMAFAG9DoUABEASQBBC1xwfwBlRABGABpfUABhABkAVQAhaNAAJgS-AAkAUWCAJQAVbIBmfNBIgAVg-yLsgBYEgF8ASkNDaCsABxxrW3LBnEcAEgA1OQU0AB5+ochKSxsiAD5DEAgBIupqFmNTczgMEaIi4WhqKxjES8IxoYdQKp2wIoAPIbJQA5HagnMyYc7QJ7XW5WHKPDbPcZvACsvWWw3hAGlYFY3pMsVZFmjVutbNtdlAAYdjiZQXxYBdMdiHiSiPi3gByYplSrsz4A34uSlAkFnekQxkwuG2NmOdkAKVwGKKpUivKAA

I'm not so sure: $Values can't work with arrays and the title of this issue is "Union from array literal"

@leebyron Specifically, the feature request is to be able to say:

const suites = Object.freeze([
  'DIAMONDS',
  'CLUBS',
  'HEARTS',
  'SPADES',
})

type Suit = $ArrayValues<typeof suites>
// No Error
const testDiamond: Suit = 'DIAMONDS'
// Expect Error
const testBogus: Suit = 'BOGUS'

I'm having issues with this when working with Mongoose. Their enum property for a field accepts an array of strings meaning there is no way for me to share this information with my class definition. There is no way around using an Array literal without adding extra logic for nothing other than a type.

I wish I could write something like this without having to maintain two separate lists.

import mongoose from 'mongoose';

const schemaDefinition = {
  type: {
    type: String,
    enum: [
      'car',
      'truck',
      'van',
    ],
    required: true,
  },
};

const schema = mongoose.Schema(schemaDefinition);

class VehicleClass {
  /** the type of vehicle */
  type: $Values<schemaDefinition.type.enum>;
}

schema.loadClass(VehicleClass);

mongoose.model('Vehicle', schema);

I've been using this workaround:

// create an object for the sake of flow
const namesObj = {
  'ava': '',
  'billy': '',
}

// create an array for actual use
const names = Object.keys(namesObj)

type Name = $Keys<typeof namesObj>

const sayMyName = (name: Name) => {
  console.log(name)
}

sayMyName('ava') // works
sayMyName(names[0]) // works
sayMyName('baz') // nah

It's not ideal, but at least means I'm only updating the 'array' in one place.

+1 for a utility to get a Union from an array literal!

@good-idea this is workable but very regrettably introducing runtime overhead to allow something that we should be able to get for free at build time...

if $ObjMap were expanded to work on array literals it could perhaps be achieved

A different use case but if you have the array defined as a type we could make it work with $Call like this:

type Enum = ['A', 'B']

const $getArrayVals = x => x.map(t => t)[0]

type $ArrayVals<T> = $Call<typeof $getArrayVals, T>

type $Letter = $ArrayVals<Enum>

const letter1: $Letter = 'A'
const letter2: $Letter = 'C' // error here

The problem is that it does not work if you try to use typeof myEnum because you'll get an array of strings here, but maybe something could be worked from that?

Note: Just in case, I also tried defining an array with casted strings but didn't work at all:

const myEnum = [('A': 'A'), ('B': 'B')]

bump. neeeeeeed this
Typescript has it since 3.4

Is this a solution for y'all? It works for my usecase.

/* @flow */

type Events = [
  {
    type: 'POPULATE_VPX_ACCESS_TOKEN',
    userAccessToken: string,
    pageAccessToken: string,
  },
  {
    type: 'LOAD_BROADCAST',
    broadcastId: string,
  },
];

type InboundCreatorStudioEvents = {
  origin: string,
  data: $ElementType<Events, number>
};

 function doThings(event: InboundCreatorStudioEvents) {
    const origin = event.origin;
    if (origin.endsWith('facebook.com')) {
      switch (event.data.type) {
        case 'POPULATE_VPX_ACCESS_TOKEN':
          const data = event.data;
          console.log(data.userAccessToken);
          console.log(data.pageAccessToken);
          // $ExpectError
          console.log(data.broadcastId);
          break;
        case 'LOAD_BROADCAST':
          console.log(event.data.broadcastId);
          // $ExpectError
          console.log(event.data.pageAccessToken);
          break;
        default:
          return;
      }
    }
}


const populateVpxAccessTokenEvent: InboundCreatorStudioEvents = {
      origin: 'facebook.com', 
      data: {type: 'POPULATE_VPX_ACCESS_TOKEN', userAccessToken: '1234', pageAccessToken: '1234'}
    }
const loadBroadcastEvent: InboundCreatorStudioEvents = {
      origin: 'facebook.com', 
      data: {type: 'LOAD_BROADCAST', broadcastId: '1234'}
    }

doThings(populateVpxAccessTokenEvent);
doThings(loadBroadcastEvent);

https://flow.org/try/#0PQKgBAAgZgNg9gdzCYAoVAXAngBwKZgCiAbngHYYDOYAvGANqphgDeTzY2+AXGAOQAFAPICAqgBkAggBVCAfQBqAgBpzJAYXWEAytrnShAaUIA5PgBp2zAK6U8AJ0kBjJ3kqVpcANblelDPYAlmQA5pYcYDgAhiF4zq7unj5kfgHBYewAvuGsVpy4eLx84kKSACJyAEIASqVl6pLa0hZ5AEb2cFEAJk5R-gCSXalBoTnZqAC6ANzoXAT9ZK1w1mRd6vZ4URhw9toY1l2BcCTkVLS5zDuBIcHD6TldW1G8ACSEMHgAtqfSBQA8JwolHMYDI1k+rQcAD5UJkZhEmFAVk4MEcyGAunBpAALdKUAAUeFIFF4CyWKzWGy2Oz2ByOgKoAEpcgBIJxwMj+MBXG7ouhE04AOh5wXhHECUDA+JFZEF5C6lAA6oEMNj8XwoFFXEtvIL2Z8+IzmWwIsxKAgVU5sVKBRRBY8MFFBXNjXkIr07PxhGIpLJFCo1JodHoDMYzNw3ab2ZyMBinudbRh7U8xaaONHKHAPoL4CF8Q6nbYHPE3B5vORGam02AM1m8Dm4HmC4LorES4ly2RK5GIsBgGA3gAPfAowj2Dr2HvpjmZ7O5-NPQXtTo9PoYQbd6scdqbLxV919AjFOpVWrlBpNPgRrfMWtzxuE4lJ5vL7oe9ddTc3vsDwjDvCjuOOxTreM51g2eaJsmjotjEcQuKWSQVvupo7lEe5Tl0eCatYMAYNeN4bPs9hkChmSoCy5HkQiqAZrGOBwDguFbHgCg4IO7ZlskDKkosyyrOsmzbLs+yHMcT7UHQJqmjKRSatqcC6vqFhgJGBa8CwcxFN6EgyPISiqBoWi6PoRimCpRaOAhHbJEUACMABMADMAAsKmtvBCRcb4-COa5fDURw5F0WA8DdJUHRvmuPFgGS-GUkJNKifSEnnNJESyfw8l4DqXh6nABogmpTwaVp-AlOUp51BezQgq+q4DEMvnOW5gXMORqCYjieL4gxTEwCxbEcdZ3lkAy3ZdbioQEmFXQRSu77jVWQA

@randalib, this requires writing type first, not getting it from literal

Has there been any update on this feature request? This would be extremely useful and a great value-add to Flowjs.

I'd like to have an array of possible values be the single source of truth if a field can only have the values represented in the array.

// @flow
const POSSIBLE_VALUES = Object.freeze(['Value1', 'Value2', 'Value3'])

type FormData = {
  fieldKey: $ArrayValues(POSSIBLE_VALUES)  // or something to this effect
}

I keep forgetting this is not possible in flow, then I search for it and I always land on this issue. Any chance that tis is going to be implemented at any point?

I've been using this workaround:

// create an object for the sake of flow
const namesObj = {
  'ava': '',
  'billy': '',
}

// create an array for actual use
const names = Object.keys(namesObj)

type Name = $Keys<typeof namesObj>

const sayMyName = (name: Name) => {
  console.log(name)
}

sayMyName('ava') // works
sayMyName(names[0]) // works
sayMyName('baz') // nah

It's not ideal, but at least means I'm only updating the 'array' in one place.

+1 for a utility to get a Union from an array literal!

This is nice, but the outcome of it is very confusing.
For example:
image

It says the type is the actual_value: string, instead of an enum of actual values.
And when you use it wrong, the error doesn't tell you anything about the enum, just that it does not exist on object literal:
image

At least, if the name is close enough you get a hint.

Was this page helpful?
0 / 5 - 0 ratings