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

Syntactic sugar for enum types #627

Closed
danvk opened this issue Jul 13, 2015 · 73 comments
Closed

Syntactic sugar for enum types #627

danvk opened this issue Jul 13, 2015 · 73 comments

Comments

@danvk
Copy link
Contributor

danvk commented Jul 13, 2015

TypeScript has this

enum Color {Red = 1, Green = 2, Blue = 4};
var c: Color = Color.Green;

I'd prefer something that looked more like object literal syntax, for simpler translation to ES5:

enum var Color = {Red: 1, Green: 2, Blue: 4};
var c: Color = Color.Green;

It looks like you can get at this using disjoint unions:

type Foo = 1 | 2;

var x: Foo = 1;  // OK
var y: Foo = 3;  // Error: number This type is incompatible with union type

So this may just be a request for a more standard syntax, or just a call out that this is how you do enumerations in FlowType.

@jeffmo
Copy link
Contributor

jeffmo commented Jul 24, 2015

Indeed disjoint unions would be the recommendation for enums (and I'd say typically string literals will be more descriptive than numbers).

Keeping this open to track documentation around this as suggested

@guizmaii
Copy link

Sorry to pollute the thread but is there any enums already implemented in Flow ? I can't find any doc about this.

@samwgoldman
Copy link
Member

@guizmaii type MyEnum = "foo" | "bar" | "baz" is what I use.

@guizmaii
Copy link

Thanks. How do you use it ?

@stri8ed
Copy link

stri8ed commented Dec 22, 2015

@samwgoldman The downside to that is you cannot do myVar = MyEnum.foo

@guizmaii
Copy link

@stri8ed +1

@vkurchatkin
Copy link
Contributor

This works nicely:

const MyEnum = {
  FOO: 'FOO',
  BAR: 'BAR'
};

type MyEnumType = $Keys<typeof MyEnum>

@jedwards1211
Copy link
Contributor

Would it be possible in the future for flow type defs to refer to const primitives? E.g.

const FOO = 'FOO';
const BAR = 'BAR';

type MyEnum = FOO | BAR;

@timzaak
Copy link

timzaak commented Mar 26, 2016

@vkurchatkin

 const Qos = {
  AT_MOST_ONCE: 0,
  AT_LEAST_ONCE: 1,
  EXACTLY_ONCE: 2,
  DEFAULT: 3,
};
type QosType = $Keys< typeof Qos>;

class Test{
  qos:QosType = Qos.AT_LEAST_ONCE;
}

will have this error:

27: qos:QosType = Qos.AT_LEAST_ONCE;
^^^^^^^^^^^^^^^^^ number. This type is incompatible with
27: qos:QosType = Qos.AT_LEAST_ONCE;
^^^^^^^ key set

@vkurchatkin
Copy link
Contributor

@timzaak yes, only works if key == value

@rafayepes
Copy link

How do you differentiate between 2 enums with same values?

Eg

type Sizes = 1 | 2;
type Colors = 2 | 3;

var x: Sizes = 2;

var y: Colors = x; // Is there a way to make this fail? Although the enums share the same values, are semantically different things

@vkurchatkin
Copy link
Contributor

@rafayepes this should fail, because x is declared as Sizes

@shinout
Copy link
Contributor

shinout commented May 28, 2016

What's the best way to export both types and values?

[export]

// @flow
export const TicketResult = {
    OK: 'OK',
    NG: 'NG',
    EXPIRED: 'EXPIRED'
}
export type TicketResultType = $Keys<typeof TicketResult>

[import]

// @flow
import type {TicketResultType} from './ticket-result'
import {TicketResult} from './ticket-result'

function getTicketResult(): TicketResultType {
    return TicketResult.OK
}

Are there better ways?

@chrisui
Copy link

chrisui commented Aug 2, 2016

Also interested in the answer to the question above this comment. In particular in cases where my values are not the same as my enum keys. Ie. From example above TicketResult.OK !== 'OK' but maybe TicketResult.OK === 1 or rather TicketResult.OK === 'ok' (since we prefer not to use int's :D)

@chrisui
Copy link

chrisui commented Aug 2, 2016

I'm imagining something like $Values<typeof TicketResult>

@avikchaudhuri
Copy link
Contributor

@shinout Yeah that pretty much does it.

@chrisui $Values should be available soon, we've been talking about it recently. cc @thejameskyle
For now you can create an explicit type 'ok' | 'ng' | 'expired' instead.

@chrisui
Copy link

chrisui commented Aug 9, 2016

$Values would be awesome. Is there a PR/Issue to track for it?

@ccorcos
Copy link

ccorcos commented Aug 25, 2016

I have a noob question. I have a defs file with a bunch of enums basically.

For example:

export const LOAN_STATUS  = {
  PENDING: 'pending',
  CURRENT: 'current',
  DUE: 'due',
  OVERDUE: 'overdue',
  PENDING_PAYMENT: 'pending_payment',
  CHARGED_OFF: 'charged_off',
  VOIDED: 'voided',
  DISPUTED: 'disputed',
  REFUNDED: 'refunded',
  SETTLED: 'settled',
}

First question is why should I need to annotate this? Its static. Second is, if I annotate as {[key: string]: string} and then import this file from elsewhere, are these values being type checked? If not, am I expected to do something like this?

type LoanStatusValues =
  'pending' |
  'current' |
  'due' |
  'overdue' |
  'pending_payment' |
  'charged_off' |
  'voided' |
  'disputed' |
  'refunded' |
  'settled'

type LoanStatusKeys =
  'PENDING' |
  'CURRENT' |
  'DUE' |
  'OVERDUE' |
  'PENDING_PAYMENT' |
  'CHARGED_OFF' |
  'VOIDED' |
  'DISPUTED' |
  'REFUNDED' |
  'SETTLED'

Then I can use this type annotation {[key: LoanStatusKeys]: LoanStatusValues}. But then I have this as well:

export const ACTIVE_LOAN_STATUS : Array<LoanStatusValues> = [
  LOAN_STATUS.OVERDUE,
  LOAN_STATUS.CURRENT,
  LOAN_STATUS.DUE,
  LOAN_STATUS.PENDING_PAYMENT,
]

Except its not any LoanStatusValues. So then I need this?

type ActiveLoanStatus = 
  "current" |
  "due" |
  "overdue" |
  "pending_payment"

export const ACTIVE_LOAN_STATUS : Array<ActiveLoanStatus> = [
  LOAN_STATUS.OVERDUE,
  LOAN_STATUS.CURRENT,
  LOAN_STATUS.DUE,
  LOAN_STATUS.PENDING_PAYMENT,
]

I'm just getting started with Flow and I'm really excited about it, but this is really starting to feel like a pain in the butt.

This is how I we're currently using these defs:

if (defs.ACTIVE_LOAN_STATUS.indexOf(loan.status) !== -1) {

}

I'd be happy to rewrite these enums with Flow, but then I cant actually use those definitions in JS:

type ActiveLoanStatus = 
  "current" |
  "due" |
  "overdue" |
  "pending_payment"

if (loan.status isTypeOf ActiveLoanStatus) {

}

Anyways, I'd appreciate some help -- or perhaps I should put this on Stack Overflow?

@philikon
Copy link
Contributor

philikon commented Sep 6, 2016

Yay for $Values (I suggested the same in #961).

@jamiebuilds
Copy link
Contributor

Just to put it down in writing on this thread, I think it'd be a mistake to implement the enum syntactic sugar for both types and values at this stage as there is talk of adding enums as an ES proposal and anything Flow adds should be based on those semantics.

@ccorcos
Copy link

ccorcos commented Sep 6, 2016

You're pretty much always going to be using an object literal to alias the actual values. Suppose you're working with an external api and don't get to choose the semantics. You might have something like this:

const Status = {
  chargedOff: 'charged-off',
}

So $values would work:

type StatusType = $Values<Status>

But I think it would be even more seamless if Flow weren't just type annotations but actually transpiled. That way you could defined an enum and its both a JS object and a union type.

enum Status = {
  chargedOff: 'charged-off',
}

@rafayepes
Copy link

@thejameskyle is there any thread related to that ES proposal we could follow? Will it be that wrong to have something in flow before a proposal is stablished? Why not use this platform to experiment and drive the proposal, instead of waiting a proposal to be stablished and drive the users? Maybe flowtype implementation could be codemoded if diverges from the ES proposal, once that comes out? Do we really want to wait until this proposal reaches stage 4?

Sorry for the rage of questions, but I'm very interested in the topic 😅

@jamiebuilds
Copy link
Contributor

There's currently not proposal that's been made into a public repo or something. I don't think Flow is the right place to drive proposals either. We don't need to wait until it's stage 4 to feel confident implementing it, but right now it's stage -1 with the desire to implement from some tc39 members

@acusti
Copy link
Contributor

acusti commented Nov 3, 2016

@thejameskyle Just to clarify, do you think Flow shouldn’t implement $VALUES to mirror $KEYS until a tc39 enum proposal is created, or are you suggesting that Flow shouldn’t create a separate enum flow type?

@antitoxic
Copy link

antitoxic commented Jan 27, 2018 via email

@augbog
Copy link

augbog commented Feb 6, 2018

Just wanted to confirm I understand what is left in this issue and maybe people who are slow to coming to this discussion can get up to speed (hope I don't seem like an idiot):

So this issue is essentially tracking support for literals correct? It sounds like @jedwards1211 pointed out some interesting cases where specific expressions may not work but IMO that's still pretty good.

@Ashoat
Copy link

Ashoat commented Feb 7, 2018

I'm in Flow 0.64, and it looks like Object.freeze lets you get around the "$Values accepts any string/number" issue:

const messageType = Object.freeze({
  TEXT: 0,
  CREATE_THREAD: 1,
});
type MessageType = $Values<typeof messageType>;
const test: MessageType = 2; // error

Unfortunately, if you're using the constant as the key to a union-of-disjoint-shapes, there's no way to define the type of the union-of-disjoint-shapes without copy-pasting the values of your constant.

type MessageInfo = {|
  type: typeof messageType.TEXT,
  text: string,
|} | {|
  type: typeof messageType.CREATE_THREAD,
  threadID: string,
|};
const test2: MessageInfo = { type: 1000, text: "hello" }; // no error

Another annoyance is that if you want to have a runtime assertion for your enum that typechecks, you need to copy-paste all the values yet again:

function assertMessageType(mType: number): MessageType {
  invariant(
    mType === messageType.TEXT || mType === messageType.CREATE_THREAD,
    "number is not MessageType enum",
  );
  return mType; // error
}

Looks like the value of messageType.TEXT is getting mis-inferred as the generic number, even though typeof messageType has the correct shape when used as a type parameter.

@andiwinata
Copy link

Tried using Object.freeze() approach, but apparently it only works if you declare the object directly inside Object.freeze().

Example

const countries = Object.freeze({ IT: 'Italy', FR: 'France' });

type Country = $Values<typeof countries>;
const a: Country = 'hello' // error (this is correct)

However this one is not working

const obj = { IT: 'Italy', FR: 'France' };
const countries = Object.freeze(obj);

type Country = $Values<typeof countries>;
const a: Country = 'hello' // no error (this is wrong...)

here's the link to the code (in flow try)

@pvinis
Copy link

pvinis commented Mar 27, 2018

any news about this?

@mgtitimoli
Copy link

mgtitimoli commented May 16, 2018

Here are my 2 cents on How to create a enum module with a very few loc and also in a way that can be consumed consistently in flow and at runtime as well:

colors.js (we use plural because it exports all the values as default)

// Pay special attention to:
// 1. We are casting each value to its own, as otherwise all of them would be strings
// 2. Freezing would be strictly required if we weren't casting each value to its
//    own (1), but as we are, its becomes optional here from the flow point of view
const all = Object.freeze({
  green: ("COLOR_GREEN": "COLOR_GREEN"),
  red: ("COLOR_RED": "COLOR_RED"),
  yellow: ("COLOR_YELLOW": "COLOR_YELLOW"),
});

type Color = $Values<typeof all>;

// optional but sometimes useful too
// we are casting to any as Object.values returns Array<mixed> and mixed can't be casted
const values: Array<Color> = Object.freeze(Object.values(all): any);

export {all as defaut, values};

export type {Color};

otherModule.js

import colors, {values as colorValues} from "./colors";
import type {Color} from "./color";

type SomethingRed = {
  color: typeof colors.red
};

const thing: SomethingRed = {color: colors.red};

const color1: Color = colors.green;
const color2: Color = colors.red;
const color3: Color = colors.yellow;

console.log(colorValues); // ["COLOR_GREEN", "COLOR_RED", "COLOR_YELLOW"]

If we weren't casting to their correspoding literal types (same value) and using Object.freeze only, then type Color = $Values<typeof all>; would continue working fine, but we weren't be able to use all.<key> anymore as each value would be of their correspondent primitive type (string in this case).

Another alternative is to define one type for each value and then use an union to define the enum, but doing it this way is longer and also requires to define a type for all to be able to use it as we are doing it in module.js.

Another good reason of using this alternative is that despite the fact that we need to write each value twice (the value itself and the casting to its literal type), it's done in the same expression which means that it's type checked, so in case we make a typo it will throw an error.

I would like with all my ❤️ that someday flow add a better way to support this pattern, but meanwhile we can live with this way of declaring enums.

@jeffersoneagley
Copy link
Contributor

I've ran in to the dreaded "flow/webstorm doesn't recognize flow's utility types" problem... Has anyone worked out a way to do this without using the utility types? I've found nothing out there on troubleshooting the error other than "use flow global instead", which didn't work,

@bluepnume
Copy link

Would love to see a resolution to this issue. This is still the best I can muster without resorting to Object.freeze:

type Enum<T> = { [string] : T };

const RGB : Enum<'red' | 'green' | 'blue'> = {
    RED:   'red',
    GREEN: 'green',
    BLUE:  'blue'
};

type RGB_ENUM = $Values<typeof RGB>

let color1 : RGB_ENUM = 'red';
let color2 : RGB_ENUM = 'gazorpazorp';

@mgtitimoli
Copy link

mgtitimoli commented Jul 4, 2018

Hi @bluepnume,

Be careful that the way you wrote doesn't fully type the values, as when you use RGB_ENUM, you are saying that particular const could have all the posibilities (because RGB_ENUM is an union), and not the one that it holds.

Take a look to this comment where I described this more extensively.

@bluepnume
Copy link

bluepnume commented Jul 5, 2018

@mgtitimoli -- isn't that fine for most use cases, when the main thing I care about is that my variable contains one of the enumerated values?

let color : RGB_ENUM = getColorFromSomewhere();

Now I can at least be sure that color is one of red, green or blue right?

EDIT: aaah, I see what you're saying now I think. (RGB.RED : 'red') wouldn't type check doing it this way. Yeah, that's a fair point.

@mgtitimoli
Copy link

mgtitimoli commented Jul 6, 2018

Exactly @bluepnume it works most of the times except for the case where you have a disjoint union where the tag is an enum (something very common), then in this case you will need to specify the exactly enum value and not the whole union, so having the possibility of doing the following would be super nice (types is the enum):

import types from "./types"

const elementOfTypeA = {type: types.a, /* rest of the values*/};

For the other cases I believe you are fine with the way you wrote.

@karlhorky
Copy link

Tried using Object.freeze() approach, but apparently it only works if you declare the object directly inside Object.freeze().

@andiwinata maybe that's worth a separate issue? Seems to me like a bug...

@karlhorky
Copy link

karlhorky commented Jul 9, 2018

  1. We are casting each value to its own, as otherwise all of them would be strings

@mgtitimoli Doesn't this work the same without the casting? Eg:

const all = Object.freeze({
  green: "COLOR_GREEN",
  red: "COLOR_RED",
  yellow: "COLOR_YELLOW",
});

type Color = $Values<typeof all>;

const purple: Color = "COLOR_PURPLE"; // error (correct)

Try Flow


If you're getting the values from elsewhere (not an object literal), then this starts to fall apart. This may be related to the issue that @andiwinata mentioned:

const colors = [
  "COLOR_GREEN",
  "COLOR_RED",
  "COLOR_YELLOW"
];

const all = Object.freeze(colors.reduce((acc, curr) => {acc[curr] = curr; return acc;}, {}));

type Color = $Values<typeof all>;

const purple: Color = "COLOR_PURPLE"; // no error (incorrect)

Try Flow

@mgtitimoli
Copy link

mgtitimoli commented Jul 9, 2018

They do work in terms of the type @karlhorky, so for Color type would be the same, the problem is if you want to also export all the values to then be used in another module as colors.green... In this particular case that will resolve to string and not to "COLOR_GREEN".

Another thing to have in mind is that it's pretty common to use enums to define the "tag" in disjoint unions, and for this case casting all the values to theirselves really help, because as I pointed before you are gonna be able to specify each disjoint union tag value directly with colors.<desired-color>.

Regarding to what you wrote at the end of having all the values in an array, I believe it's related with the same thing, that is flow doesn't type any primitive value as the value itself (literal type) and instead the resulting type is the corresponding primitive (f.e.: "value" type is string and not "value"), and this is the reason why reducing them ends up with and object where all the values are strings.

@vikingair
Copy link

Finally I found the probably best way to define enums... But you have to define your enum in a separate js file to make opaque work corretly. The enum itself:

export opaque type Color: string = 'COLOR_GREEN' | 'COLOR_RED' | 'COLOR_YELLOW';

export const Colors = {
    Green: ('COLOR_GREEN': Color),
    Red: ('COLOR_RED': Color),
    Yellow: ('COLOR_YELLOW': Color),
};

And any other js code:

const green: Color = Colors.GREEN; // no error
const red: string = Colors.RED; // no error
const blue: Color = 'BLUE'; // error !!!
const yellow: Color = 'COLOR_YELLOW'; // error !!! -> because opaque types can only be created in the same file

const useColor = (color: Color) => { ... };
const readColor = (color: string) => { ... };

useColor(green); // no error
readColor(green); // no error
useColor(red); // error !!! -> because it was (down-)casted to string
readColor(red); // no error

Is this finally the solution to the problem?

@mgtitimoli
Copy link

mgtitimoli commented Nov 7, 2018

That works @fdc-viktor-luft, but you are casting all your values to the union, so you are loosing the possibility to use each value independently.

Mainly because of what I wrote above is why I ended up defining enums the way I described here.

@vikingair
Copy link

vikingair commented Nov 8, 2018

To be honest, I don't quite see your point @mgtitmoli. Using the example above. For me it is important that a Color can be used as string, but a somewhere else created string should never be usable as my enum Color.

But if you do want the color to be not usable as string itself. You only have to change one line from above:

export opaque type Color = 'COLOR_GREEN' | 'COLOR_RED' | 'COLOR_YELLOW';

Notice that I leaved the ": string" down-cast. Then a Color can not be used as string anymore and you would have to provide an additional toString-Function.

@mgtitimoli
Copy link

mgtitimoli commented Nov 8, 2018

Hi @fdc-viktor-luft,

By doing this:

export opaque type Color = 'COLOR_GREEN' | 'COLOR_RED' | 'COLOR_YELLOW';

export const Colors = {
    Green: ('COLOR_GREEN': Color),
    Red: ('COLOR_RED': Color),
    Yellow: ('COLOR_YELLOW': Color),
};

And not this:

const colors = {
    green: ("COLOR_GREEN": "COLOR_GREEN"),
    red: ("COLOR_RED": "COLOR_RED"),
    yellow: ("COLOR_YELLOW": "COLOR_YELLOW"),
};

type Color = $Values<typeof colors>;

export default colors;

export type {Color};

You are telling flow that each enum value is the type of the enum and not of itself, so if you later would like to use a single value of the enum, you won't be possible.

In the other hand, if you use the other way, you can get the type of each enum value independently.

So, to summarize,

The way you wrote: typeof Colors.Green => Color

The other way: typeof colors.green => "COLOR_GREEN" (which is one of the possible enum values)

As a side note I would also recommend to name the enum module in plural, so you would import it:

// @flow
import colors from "./colors";

import type {Color} from "./colors";

@bluepnume
Copy link

Yep, I've been using @mgtitimoli's approach for some time now and it works great. Really still hoping the Flow team can incorporate this as a first-class feature. It's very common for enum values to be different from the keys.

@vikingair
Copy link

vikingair commented Nov 9, 2018

@mgtitimoli and @bluepnume I've got your point why you are prefering the other solution, but in my opinion those are no real enums. If you look on any other programming language with enums you would never expect certain enum types as parameters of a function or anything else. Let's get some examples.

import { Colors, type Color } from './path/to/colors.js';

const applyColorToStyle = (color: Color, style: Object) => ({ ...style, color });

const ColoredComponent = ({ color: Color }) => <div style={applyColorToStyle(color, {fontSize: '1.5em'}))}>Content</div>;

const SomeComponent = () => (
    <>
        <ColoredComponent color={Colors.RED} />
        <ColoredComponent color={Colors.BLUE} />
    </>
);

In e.g. JAVA you have got something like:

enum Color {
    Green('COLOR_GREEN'),
    Red('COLOR_RED'),
    Yellow('COLOR_YELLOW');

    private final String _value;

    Color(final String value) { _value = value; }

    public String getValue() { return _value; }
}

// And use it like so
public void printColorValue(final Color thatColor) { System.out.println(thatColor.getValue()); }

// I find no necessary use case for doing something like this
public void printRedColorValue(final RedColor thatColor) { System.out.println(thatColor.getValue()); }
// because I would rather do
public void printRedColorValue() { System.out.println(Color.RED.getValue()); }

In your example it is still possible to create enum instances out of nowhere, like so:

import type { Color } from "./colors";

const somethingRed: Color = 'COLOR_RED'; // without the usage of your colors export

I know that with my solution something like your SomethingRed type is not possible, but why should it be? Why do you not insert the Red-Color yourself instead of ensuring that something red will come to you? Or making runtime checks like:

const doSomethingWithRed = (color: Color) => {
    if (color === Colors.RED) run();
    else if (color === Colors.GREEN) that();
    else console.log('wrong color'); // like any other programming language
}

Maybe instead of persisting that your solution is more like any enum, you could provide me a valid example of code, where it is necessary to have a SomeSpecificEnumValue-Type

@arv
Copy link

arv commented Nov 9, 2018

One of the reasons SomethingRed is useful is because it allows the type inferrer to work better. It can catch unused/missing case statements and it can detect dead code as well as do type narrowing on branches. If you lose/remove that distinction between Colors.RED and Colors.BLUE you are missing out a lot of nice features of Flow.

@Ailrun
Copy link

Ailrun commented Nov 10, 2018

For ex)

function exhaustiveCheck(x: empty) {
  throw new TypeError("Impossible case is detected");
}

function example(x): number {
  switch (x) {
    case Colors.RED:
      return 5;
    case Colors.GREEN:
      return 4;
    case Colors.YELLOW:
      return 3;
    default:
      return exhaustiveCheck(x);
  }
}

will not give any error on exhaustiveCheck when you have distinct types for each of these enum values, and will give errors on exhaustiveCheck when you don't have those.

@vikingair
Copy link

I have to admit. The exhaustiveCheck does not work with my suggestion 😭

@gkz
Copy link
Member

gkz commented Sep 13, 2021

Closed with https://medium.com/flow-type/introducing-flow-enums-16d4239b3180

@gkz gkz closed this as completed Sep 13, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests