flow icon indicating copy to clipboard operation
flow copied to clipboard

Union from array literal

Open philikon opened this issue 10 years ago • 29 comments
trafficstars

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.

philikon avatar Oct 20 '15 18:10 philikon

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.

STRML avatar Jul 08 '16 19:07 STRML

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

bb010g avatar Dec 15 '16 02:12 bb010g

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

mull avatar Jun 08 '17 15:06 mull

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

wbinnssmith avatar Jun 20 '17 02:06 wbinnssmith

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

STRML avatar Jun 20 '17 02:06 STRML

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

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

vkurchatkin avatar Jun 20 '17 05:06 vkurchatkin

Is that planned?

On Jun 20, 2017 12:05 AM, "Vladimir Kurchatkin" [email protected] 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 .

STRML avatar Jun 20 '17 12:06 STRML

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.

vkurchatkin avatar Jun 20 '17 17:06 vkurchatkin

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.

STRML avatar Jun 20 '17 17:06 STRML

@STRML I don't think that's right. See https://github.com/facebook/flow/blob/ab0789ade95090c2a07ce1dff0e6511226ed73fd/tests/values/object_types.js#L80

philikon avatar Jul 24 '17 19:07 philikon

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 avatar Jul 24 '17 19:07 philikon

@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 avatar Jul 24 '17 19:07 STRML

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

philikon avatar Jul 24 '17 19:07 philikon

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?

aguynamedben avatar Apr 18 '18 04:04 aguynamedben

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

leebyron avatar Jul 09 '18 08:07 leebyron

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

apsavin avatar Jul 09 '18 08:07 apsavin

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

asazernik avatar Jul 10 '18 03:07 asazernik

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

SavePointSam avatar Jul 17 '18 20:07 SavePointSam

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 avatar Aug 05 '18 23:08 good-idea

@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

ericketts avatar Aug 18 '18 22:08 ericketts

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')]

Zaggen avatar Sep 14 '18 15:09 Zaggen

bump. neeeeeeed this Typescript has it since 3.4

avalanche1 avatar Jun 14 '19 13:06 avalanche1

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

randallb avatar Jul 30 '19 17:07 randallb

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

goodmind avatar Jul 30 '19 18:07 goodmind

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
}

thedanchez avatar Mar 02 '20 19:03 thedanchez

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?

danielo515 avatar Dec 10 '20 12:12 danielo515

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.

danielo515 avatar Dec 10 '20 12:12 danielo515

You can use one of the above solutions, or use Flow Enums: https://flow.org/en/docs/enums/

gkz avatar Feb 16 '23 02:02 gkz

To clarify the above comment - someEnum.members() is a method which does what I and I think most other people in this thread wanted: single source of truth, no boilerplate, iterable, and typesafe.

asazernik avatar Feb 16 '23 07:02 asazernik