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

Suggestion: Structually assignable enum #22373

Closed
tamayika opened this issue Mar 7, 2018 · 12 comments
Closed

Suggestion: Structually assignable enum #22373

tamayika opened this issue Mar 7, 2018 · 12 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@tamayika
Copy link

tamayika commented Mar 7, 2018

Currently ts enum is nominal.
Even if two enums have the save content, they are not assignable each other.

enum A {
    foo,
}

enum B {
    foo,
}

const a: A = B.foo; // Error: Type 'B' is not assignable to type 'A'.
const b: B = A.foo; // Error: Type 'A' is not assignable to type 'B'.

This is good for the safety of coding at some point.

But please think below example.

enum JobStateAtServer {
    enqueued = "enqueued",
    running = "running",
    success = "success",
    failed = "failed",
}

enum JobStateAtClient{
    enqueuing = "enqueuing",
    enqueued = "enqueued",
    running = "running",
    success = "success",
    failed = "failed",
}

declare function enqueueJob(): Promise<JobStateAtServer>;
declare function getJobState(): Promise<JobStateAtServer>;

(async () => {
    let jobState = JobStateAtClient.enqueuing;
    jobState = await enqueueJob(); // Error: Type 'JobStateAtServer' is not assignable to type 'JobStateAtClient'.
    while (jobState != JobStateAtClient.failed && jobState != JobStateAtClient.success) {
        jobState = await getJobState(); // Error: Type 'JobStateAtServer' is not assignable to type 'JobStateAtClient'.
    }
});

Please assume JobStateAtServer, enqueueJob and getJobState is generated by api.json(like swagger).
JobStateAtClient is defined at client user code and extended with the state enqueuing which means before enqueue.

To solve this problem, I suggest following structual keyword for enum.

enum A {
    foo = "foo"
}

enum B {
    foo = "foo"
}

structual enum C {
    foo = "foo"
}

enum D {
    foo = "foo",
    bar = "bar"
}

structual enum E {
    foo = "foo",
    bar = "bar",
}

let a: A;
let b: B;
let c: C;
let d: D;
let e: E;
let f: "foo" | "bar";
let g: "foo" | "bar" | "baz";

a = b; // Error
a = c; // Error
c = a; // OK
c = d; // Error
e = f; // OK
e = g; // Error
  • Only when the type of assigned value is structual enum,
    • if the enum values of assigned value includes the enum values of assigning value, it's assignable.
    • if the enum values of assigned value includes the type literals of assigning value, it's assignable.
@ghost
Copy link

ghost commented Mar 7, 2018

You could just use a string literal union instead of an enum?

type JobStateAtServer = "enqueued" | "running" | "success" | "failed";
type JobStateAtClient = "enqueuing" | "enqueued" | "running" | "success" | "failed";
let s: JobStateAtServer = "enqueued";
let c: JobStateAtClient = s; // no error

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature labels Mar 7, 2018
@RyanCavanaugh
Copy link
Member

This is also largely equivalent to

namespace E {
  export const
    foo = "foo",
    bar = "bar";
}
type E = typeof E;

@tamayika
Copy link
Author

tamayika commented Mar 7, 2018

You could just use a string literal union instead of an enum?

Yes, it's true. But string literails can not get IDE input support and weak for typo like below.

type JobStateAtServer = "enqueued" | "running" | "success" | "failed";
type JobStateAtClient = "enqueuing" | "enqueued" | "running" | "success" | "failed";

declare function enqueueJob(): Promise<JobStateAtServer>;
declare function getJobState(): Promise<JobStateAtServer>;

(async () => {
    let jobState = "enqueuing";
    jobState = await enqueueJob(); // OK
    // string literal is not safe for typo
    while (jobState != "failedwithtypo" && jobState != "success") {
        jobState = await getJobState(); // OK
    }
});

@ghost
Copy link

ghost commented Mar 7, 2018

The only reason you don't get an error at the comparison there is because jobState is initialized to a string literal, so the inferred type is string and not a string literal union. You'll get an error at the bad comparison if you give it an explicit type annotation, or initialize it with let jobState = await enqueueJob();.

@tamayika
Copy link
Author

tamayika commented Mar 7, 2018

if you give it an explicit type annotation

That's right.

type JobStateAtServer = "enqueued" | "running" | "success" | "failed";
type JobStateAtClient = "enqueuing" | "enqueued" | "running" | "success" | "failed";

declare function enqueueJob(): Promise<JobStateAtServer>;
declare function getJobState(): Promise<JobStateAtServer>;

(async () => {
    let jobState: JobStateAtClient = "enqueuing";
    jobState = await enqueueJob(); // OK
    // OK and I can get IDE input support.
    while (jobState != "failed" && jobState != "success") {
        jobState = await getJobState(); // OK
    }
});

OK, i give up using enum...

Thanks for reply and some solutions!

@tamayika tamayika closed this as completed Mar 7, 2018
@tamayika
Copy link
Author

tamayika commented Mar 8, 2018

After closing issue, I got some notice around this.
(But not so important, I just record for someone who will see this issue by search. So I keep this issue closed.)

First, I said "// OK and I can get IDE input support.".
However, this is only typescript playground(monaco editor with typescript language mode) but not in VSCode.
VSCode suppress suggestion when typing string literals by default.
To avoid this, I must edit settings.json

{
    "editor.quickSuggestions": {
        "other": true,
        "comments": false,
        "strings": true // <- here
    }
}

But this is so annoying because suggestion have top level keywords like class, etc.
TS team has already known this problem and will tackle this problem at #21012.

Second, I can not rename string literals with IDE support.
But if I use enum, enum members have symbol, so tsserver can replace all referenced position.

@ghost
Copy link

ghost commented Mar 8, 2018

I can not rename string literals

With the default vscode settings this should work:

const x = "abc";
const y = "abc";

Right-click on either "abc" and rename, and both string literals should change.

@tamayika
Copy link
Author

tamayika commented Mar 8, 2018

Right-click on either "abc" and rename, and both string literals should change.

I confirmed it. But this is for replacing all "abc" string literals in project.
If some string literals conflict among several contexts, this replacing is very dangerous.

Also, I've noticed "Find All Reference" is not valid for string literals.

@ghost
Copy link

ghost commented Mar 8, 2018

If some string literals conflict among several contexts, this replacing is very dangerous.

That's how structural types work -- they match based on the names of things, not based on the meaning. If structural types are dangerous but you want one enum to subtype another, you could do something like:

enum JobStateAtClient{
    enqueuing = "enqueuing",
    enqueued = "enqueued",
    running = "running",
    success = "success",
    failed = "failed",
}
type JobStateAtServer = JobStateAtClient.enqueuing | JobStateAtClient.running | JobStateAtClient.success | JobStateAtClient.failed;

@tamayika
Copy link
Author

tamayika commented Mar 9, 2018

you could do something like

Yes, if I could. But JobStateAtServer is first defined(by api.json like swagger) and JobStateAtClient is extended with user states. So its definition is not right order.
If I can extend enum, this also solves the issue.

enum JobStateAtServer {
    enqueued = "enqueued",
    running = "running",
    success = "success",
    failed = "failed",
}

enum JobStateAtClient extends JobStateAtServer {
    enqueuing = "enqueuing",
}

declare function enqueueJob(): Promise<JobStateAtServer>;
declare function getJobState(): Promise<JobStateAtServer>;

(async () => {
    let jobState = JobStateAtClient.enqueuing;
    jobState = await enqueueJob(); // this needs that enum is not nominal
    while (jobState != JobStateAtClient.failed && jobState != JobStateAtClient.success) {
        jobState = await getJobState(); // this needs that enum is not nominal
    }
});

In this case,

  • Only when the type of assigned value is enum,
    • if the enum of assigning value extends the enum of assigned value, it's assignable.

So, it's cheaper to check if assignable.

@ghost
Copy link

ghost commented Mar 12, 2018

The syntax isn't as nice but you could emulate extends with:

enum JobStateAtServer {
    enqueued = "enqueued",
    running = "running",
    success = "success",
    failed = "failed",
}

enum ClientSpecificJobState {
    enqueuing = "enqueuing",
}

type JobStateAtClient = JobStateAtServer | ClientSpecificJobState;
// Can omit this namespace if you're willing to write `JobStateAtServer` or `ClientSpecificJobState` depending on which enum it comes from
namespace JobStateAtClient {
    export const enqueued: JobStateAtClient = JobStateAtServer.enqueued;
    export const running: JobStateAtClient = JobStateAtServer.running;
    export const success: JobStateAtClient = JobStateAtServer.success;
    export const failed: JobStateAtClient = JobStateAtServer.failed;
    export const enqueuing: JobStateAtClient = ClientSpecificJobState.enqueuing;
}

@tamayika
Copy link
Author

you could emulate extends with:

Thanks, it emulates the syntax correctly.
But it's so annoying to type the same things again and again.
It remembered me string enums before they exist.

@microsoft microsoft locked and limited conversation to collaborators Jul 25, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

2 participants