Flow: Syntactic sugar for enum types

Created on 13 Jul 2015  Â·  73Comments  Â·  Source: facebook/flow

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.

Needs docs feature request

Most helpful comment

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;

All 73 comments

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

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

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

Thanks. How do you use it ?

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

@stri8ed +1

This works nicely:

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

type MyEnumType = $Keys<typeof MyEnum>

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;

@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

@timzaak yes, only works if key == value

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

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

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?

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)

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

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

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

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?

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

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.

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',
}

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

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

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

$Values would be fine, I was just talking about an enum syntax. Although @jeffmo seemed concerned the other day about adding a $Values in regards to soundness

What are the concerns re: $Values when applied to array and object literals with primitive value types (number, string, bool)?

Although @jeffmo seemed concerned the other day about adding a $Values in regards to soundness

Hmm. What sort of concerns here? Are you worries that $Values may interpret things as string and number rather than literals?

Anecdotally, my team would find $Values useful. Our enums currently look like this:

const FooTypes = {
    BAR: 'bar',
    BAZ: 'baz',
    QUX: 'qux',
};

Since the values are stored in the database and are part of the public interface between services, they're not easy to change. So we'd like to be able to Flow-type these enums as:

type FooType = $Values<typeof FooTypes>;

@kasrak this is not going to work anyway, since Flow infers object type as { BAR: string, BAZ: string, QUX: string, }

If we freeze FooTypes then flow should infer them as constants. (I don't think Flow currently does this)

const FooTypes = Object.freeze({
    BAR: 'bar',
    BAZ: 'baz',
    QUX: 'qux',
});

type FooType = $Values<typeof FooTypes> // 'bar' | 'baz' | 'qux'

Is there a way to make $Keys work when importing from .json?

{
 "FOO": "FOO",
 "BAR": "BAR"
}
// @flow
import MyEnum from './file_above.json' // require() doesn't change the result

type MyEnumType = $Keys<typeof MyEnum>

const noErrorTriggered: MyEnumType = "BAZ"

When importing an object literal from .js, Flow correctly throws a "Property not found ...":

// @flow
export default {
 FOO: "FOO",
 BAR: "BAR"
}

Is there any progress for this issue? Lots of requests but not quite any talk of wontfix/willfix/progress.

Here's how I use it, don't know why $Values isn't implemented yet tho...
https://gist.github.com/hejrobin/6e869684f5e493681d7e171d818b8802

Here's a working example at Babelio.js.

@hejrobin I don't think this implementation of $Values is capable of inferring any type more precise than string for an enum object's values. Flow doesn't automatically assume literal types for values in any case that I am aware of.

@hejrobin
Even worse than @ajhyndman said, that $Values<...> is nothing more than any, especially with Enumerable which is almost just a Object.
You can check it with

basicUser.role = 'test string....';
basicUser.role = 5;
basicUser.role = {
  hi: "I broke things"
};

Happily, it looks like $Values should land in a public release of flow, soon!

https://github.com/facebook/flow/commit/ab0789ade95090c2a07ce1dff0e6511226ed73fd

@ajhyndman
Oh, with real implimentation by .ml!
Hooray!

$Values is nice, but it's kind of useless if it doesn't work on arrays and you can't use arrays to assert that a string is a valid member of the enum (see #4454):

 35: export type ExternalSource = $Values<typeof EXTERNAL_SOURCES>;
                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ values type. Expected object instead of
 35: export type ExternalSource = $Values<typeof EXTERNAL_SOURCES>;
                                          ^^^^^^^^^^^^^^^^^^^^^^^ array literal

Since ES6 I prefer the following solution for flow:

let __MyType_LOCK__ = false;
export class MyType {
    value: string;
    constructor(value: string) {
        if (__MyType_LOCK__) {
            throw new Error('Enum instances have to be initialized here!')
        }
        this.value = value;
    }
    static ONE = new MyType('one');
    static TWO = new MyType('two');
}
__MyType_LOCK__ = true;

Why? - It is flexible, type safe, readable and (if you use the lock) not extendable from outside.

MyType.ONE instanceof MyType

I'm a fan of the proposal by @jedwards1211 where const variables can be used in a type declaration à la type MyEnum = A | B | C. Is there a reason that's not allowed?

@aleclarson I should refine that by saying assignment of literals would be supported:

const foo = 'bar'
const baz = 2
const trump = {
  pants: 'on fire',
}

type WhatTheHeck = foo | baz | trump

But any other expressions on the RHS might not:

const someJson = JSON.stringify({hello: 'world'})
const four = 2 + 2

type NoGo = someJson | four

@jedwards1211 do you know of any PR, roadmap or issue on the support of literals?

No, I'm kind of astounded how many upvotes that suggestion of mine received! I've found that $Keys is usually sufficient for what I'm doing because I often have a map from enum constants to specific properties associated with each constant like display text and so forth. So I haven't really looked into what I suggested in a long time.

The following would work if only typeofthe variables gave their literal value instead of string:

// @flow

const foo = 'foo'
const bar = 'bar'

type Foobar = typeof foo | typeof bar

const baz: Foobar = 'baz'

@jedwards1211 all magical $* like $Keys should be avoided. Having literal constants as enums is intuitive. Flow already does code interpretation, aka it already knows what is a literal constant.

I can't understand why is this not a thing yet.

all magical $* like $Keys should be avoided.

Documented $-types are ok to use, and there is definitely nothing wrong with $Keys.

They should be avoided because they are less readable. What's wrong with
constants as enumeration type? Constants are more readable and you don't
need documentation — as I said before, it's intuitive.

On 27 Jan 2018 14:43, "Vladimir Kurchatkin" notifications@github.com
wrote:

all magical $* like $Keys should be avoided.

Documented $-types are ok to use, and there is definitely nothing wrong
with $Keys.

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

@antitoxic and types like React.ElementConfig aren't magical? You're just afraid of the $.

I don't use or know what you are referring to by React.ElementConfig so
it's obviously off topic. But I guess it's not even remotely similar to
basic enum functionality which you are suggesting that symbols like $ are
perfectly ok to be required. We're talking about generic typization syntax.

I don't see a response to the constants used as enumeration. Let's focus on
that.

On 27 Jan 2018 18:07, "Andy Edwards" notifications@github.com wrote:

@antitoxic https://github.com/antitoxic and types like
React.ElementConfig aren't magical? You're just afraid of the $.

—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/facebook/flow/issues/627#issuecomment-360994694, or mute
the thread
https://github.com/notifications/unsubscribe-auth/AAP9fNj4j_PvjQwr4lUQUFP23jtoOmS1ks5tO0nRgaJpZM4FXpag
.

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.

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.

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)

any news about this?

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 :heart: that someday flow add a better way to support this pattern, but meanwhile we can live with this way of declaring enums.

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,

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

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.

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

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.

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

  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

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.

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?

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

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

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.

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

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.

@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

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.

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.

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

Of interest: #7837

Was this page helpful?
0 / 5 - 0 ratings