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

Union from array literal #961

Open
philikon opened this issue Oct 20, 2015 · 29 comments
Open

Union from array literal #961

philikon opened this issue Oct 20, 2015 · 29 comments

Comments

@philikon
Copy link
Contributor

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.

@STRML
Copy link
Contributor

STRML commented Jul 8, 2016

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.

@bb010g
Copy link

bb010g commented Dec 15, 2016

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

@mull
Copy link

mull commented Jun 8, 2017

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

@wbinnssmith
Copy link
Contributor

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

@STRML
Copy link
Contributor

STRML commented Jun 20, 2017

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

@vkurchatkin
Copy link
Contributor

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

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

@STRML
Copy link
Contributor

STRML commented Jun 20, 2017 via email

@vkurchatkin
Copy link
Contributor

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.

@STRML
Copy link
Contributor

STRML commented Jun 20, 2017

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.

@philikon
Copy link
Contributor Author

@STRML I don't think that's right. See

magicTrick('Magic'); // Error: 'Magic' is not a value.

@philikon
Copy link
Contributor Author

philikon commented Jul 24, 2017

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.

@STRML
Copy link
Contributor

STRML commented Jul 24, 2017

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

@philikon
Copy link
Contributor Author

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

@aguynamedben
Copy link

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?

@leebyron
Copy link
Contributor

leebyron commented Jul 9, 2018

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

@apsavin
Copy link
Contributor

apsavin commented Jul 9, 2018

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

@asazernik
Copy link

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

@SavePointSam
Copy link

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

@good-idea
Copy link

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!

@ericketts
Copy link

ericketts commented Aug 18, 2018

@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

@Zaggen
Copy link

Zaggen commented Sep 14, 2018

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

@avalanche1
Copy link

avalanche1 commented Jun 14, 2019

bump. neeeeeeed this
Typescript has it since 3.4

@randallb
Copy link
Contributor

randallb commented Jul 30, 2019

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

@goodmind
Copy link
Contributor

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

@thedanchez
Copy link

thedanchez commented Mar 2, 2020

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
}

@danielo515
Copy link

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
Copy link

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.

@gkz
Copy link
Member

gkz commented Feb 16, 2023

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

@asazernik
Copy link

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.

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

Successfully merging a pull request may close this issue.