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

Different keys and values #5

Closed
onury opened this issue Feb 11, 2017 · 8 comments · Fixed by #9
Closed

Different keys and values #5

onury opened this issue Feb 11, 2017 · 8 comments · Fixed by #9

Comments

@onury
Copy link

onury commented Feb 11, 2017

How would I get an enum having different keys and values like this:

{
    RUNNING: 'running',
    STOPPED: 'stopped'
}

so I can do:

console.log(Status.RUNNING); // —> "running"
@dphilipson
Copy link
Owner

dphilipson commented Feb 11, 2017

Hi onury,

Unfortunately I haven't found a good way to do this. All my attempts have failed because the only way I know of to produce a type that TypeScript understands as an object with string literal values that are different from its keys is to write one yourself by hand. So you may be forced to write the following, which works:

const Status = {
    RUNNING: "running" as "running",
    STOPPED: "stopped" as "stopped"
};

There is another option that I toyed with a bit:

export function WeirdEnum<K extends string>(object: { [key: K]: string }): { [k in K]: k } {
    return object as any;
}

export type WeirdEnum<T> = T[keyof T];

then use it as

export const Status = WeirdEnum({
    RUNNING: "running",
    STOPPED: "stopped"
});
export type Status = WeirdEnum<typeof Status>;

console.log(Status.RUNNING); // -> "running"

// Enum can be used for discriminated unions:

type State = RunningState | StoppedState;

interface RunningState {
    status: typeof Status.RUNNING;
    pid: number;
}

interface StoppedState {
    status: typeof Status.STOPPED;
    shutdownTime: Date;
}

function saySomethingAboutState(state: State) {
    // The following typechecks.
    if (state.status === Status.RUNNING) {
        console.log("The pid is " + state.pid);
    } else if (state.status === Status.STOPPED) {
        console.log("The shutdown time is " + state.shutdownTime);
    }
}

So this works, in a way. The issue is that even though Status.RUNNING actually has a value of "running", TypeScript is convinced that its type is "RUNNING". You'll be fine as long as you only ever use your enum constants and never string literals (i.e. only ever write Status.RUNNING throughout your program and never "running"), but if you do use string literals you'll run into weirdness:

const status: Status = "running"; // Type error: "running" not assignable to type "RUNNING" | "STOPPED".

I decided not to include this in this project because I feel like lying to TypeScript like this is a pretty sketchy thing to do, but it is there as an option if you think it's worth it.

I would be pretty happy to see if someone can come up with a better solution.

@onury
Copy link
Author

onury commented Feb 11, 2017

@dphilipson, thanks for the detailed answer.
How about if you assign also the values as keys? Such as:

{
    RUNNING: 'running',
    STOPPED: 'stopped',
    running: 'running',
    stopped: 'stopped'
}

@dphilipson
Copy link
Owner

@onury, oh I'm really sorry, I somehow missed that you had responded to this thread. I apologize for the really slow response.

Can you elaborate a bit on the question? Is that snippet something that you would like to be able to express using the library, or a proposed solution to the problems in the previous post?

@onury
Copy link
Author

onury commented Mar 2, 2017

No problem at all.
It's a suggestion to avoid Type error: "running" not assignable to type "RUNNING" | "STOPPED".

Correction: You could auto-assign the values.
user passes:

{
    RUNNING: 'running',
    STOPPED: 'stopped'
}

You auto-assign the values as keys, if they're different:

{
    RUNNING: 'running',
    STOPPED: 'stopped',
    running: 'running',
    stopped: 'stopped'
}

@onury
Copy link
Author

onury commented Mar 2, 2017

Another example:
user passes:

{
    SERVER_ERROR: 'ServerError',
    CLIENT_ERROR: 'client:error'
}

auto-generate:

{
    SERVER_ERROR: 'ServerError',
    CLIENT_ERROR: 'client:error',
    ServerError: 'ServerError',
    'client:error': 'client:error'
}

@dphilipson
Copy link
Owner

Ah, interesting idea. I'm not sure offhand how to express that through the type system (writing the function signature is the hard part), but I'll experiment.

This does have a few drawbacks I assume would come up if you managed to nail down the type signature:

  1. The reverse problem isn't fixed- instead of good values being rejected, you also have bad values being accepted:
const status: Status = "RUNNING"; // Typechecks, even though it shouldn't.
  1. You'll get extraneous values in your autocomplete. So for example if I type Status., autocomplete will suggest RUNNING, running, STOPPED, stopped, which is a little annoying.

  2. It prevents you from doing exhaustiveness checking, as described midway down this page. TypeScript won't consider all cases handled unless you provide cases for each of the duplicate values.

@onury
Copy link
Author

onury commented Mar 4, 2017

You're right. This would be very hacky.

@dphilipson
Copy link
Owner

This can now be done in 0.2.0 with the new object syntax. The following now works:

export const Status = Enum({
    /**
     * Everything is fine.
     *
     * Hovering over Status.RUNNING in an IDE will show this comment.
     */
    RUNNING: "running",

    /**
     * All is lost.
     */
    STOPPED: "stopped",
});
export type Status = Enum<typeof Status>;

console.log(Status.RUNNING); // -> "running"

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

Successfully merging a pull request may close this issue.

2 participants