Skip to content

Number literal should not extend enum of different value #31834

Closed

Description

@AnyhowStep

TypeScript Version: 3.4.1, 3.5.1

Search Terms: number literal, extend, enum

Code

enum E {
    A = 1,
    B = 2,
}
/**
    Expected: "y"
    Actual  : "y"
    OK!
*/
type oneShouldExtendA = 1 extends E.A ? "y" : "n";
/**
    Expected: "n"
    Actual  : "y"
    ???
*/
type oneShouldNotExtendB = 1 extends E.B ? "y" : "n";

/**
    Expected: "y"
    Actual  : "y"
    OK!
*/
type aShouldExtend1 = E.A extends 1 ? "y" : "n";
/**
    Expected: "n"
    Actual  : "n"
    OK!
*/
type bShouldNotExtend1 = E.B extends 1 ? "y" : "n";

Expected behavior:

oneShouldNotExtendB should be "n"

Actual behavior:

oneShouldNotExtendB is "y"

Playground Link: Playground

Related Issues:

Kind of feels like a different version of #28654 (where true extended false)

Activity

  1. AnyhowStep commented on Jun 9, 2019

    @AnyhowStep
    ContributorAuthor

    #12647 seems somewhat related

    I must say that I don't like this behaviour of numeric enums.

    Luckily, I don't use numeric enums often. Mostly string enums. But that's why I expected numeric enums to work the same.

    The whole bit flag thing sounds like a terrible idea. If one is sure (Enum.A | Enum.B) is of type Enum, they should be the ones who are inconvenienced and should add an explicit cast, if TS won't do type-level bitwise-or and check assignability.

    Allowing arbitrary numbers to assign to numeric enum types just takes away from the whole type safety thing. It's enough of a deal breaker that I just can't use numeric enums at all.

    I tried using numeric enums for a project this weekend and it's just been thoroughly unpleasant.

    It's bad for generic code, bad for non-generic code, bad as an index signature, bad in mapped object types, bad for discriminated unions, bad for type safety.

    It's a real shame because now I have to union the 50+ http status codes in existence, rather than have a single numeric enum with 50+ elements.

    I was getting weird problems where I could assign 200 to an enum called Non2xxStatusCode. Thank God I write compile-time tests and run-time tests.

    /Nerd-rage


    I understand breaking existing functionality is usually a bad idea but how often is the existing behavior actually desirable?

    When I've wanted bit masks, I used int types. Maybe have an enum to represent each flag. But the mask itself is never an enum.

    I mean, each flag means 2^(# flags) possible mask values. Unless your enum has 2^(# flags) elements, it really should be an int.

    If we can't break the existing behavior, can we introduce a different enum type that behaves like an actual numeric enum?

    The existing workaround with namespaces and type unions is just a tonne of boilerplate.

    And I would like to actually use a numeric enum, see StatusCode.NOT_FOUND, and have it behave like a regular enum.

    Not use some workaround and see 404, 419, 512, etc. How many developers even remember all the uncommon status codes off the top of their head?

    /Extra-nerd-rage

  2. AnyhowStep commented on Jun 9, 2019

    @AnyhowStep
    ContributorAuthor

    Just adding some code as a note to myself,

    enum E {
        A = 1,
        B = 2,
    }
    declare const discriminated: (
        {
            d: E.A,
            value: "A",
        } |
        {
            //E.B is 2
            //2 is 2
            d: E.B | 2,
            value: "B",
        }
    );
    if (discriminated.d == 1) {
        /**
            Expected: Assignment works
            Actual  : Type '"A" | "B"' is not assignable to type '"A"'.
        */
        const v: "A" = discriminated.value;
    }
    if (discriminated.d == 2) {
        /**
            Expected: Assignment works
            Actual  : Type '"A" | "B"' is not assignable to type '"B"'.
        */
        const v: "B" = discriminated.value;
    }
    
    if (discriminated.d == E.A) {
        /**
            Expected: Assignment works
            Actual  : Type '"A" | "B"' is not assignable to type '"A"'.
        */
        const v: "A" = discriminated.value;
    }
    if (discriminated.d == E.B) {
        /**
            Expected: Assignment works
            Actual  : Type '"A" | "B"' is not assignable to type '"B"'.
        */
        const v: "B" = discriminated.value;
    }
    /////////////////////////////////////////////////////
    
    declare const discriminated2: (
        {
            d: E.A,
            value: "A",
        } |
        {
            //E.B is 2
            d: E.B,
            value: "B",
        }
    );
    if (discriminated2.d == 1) {
        /**
            Expected: Assignment works
            Actual  : Type '"A" | "B"' is not assignable to type '"A"'.
        */
        const v: "A" = discriminated2.value;
    }
    if (discriminated2.d == 2) {
        /**
            Expected: Assignment works
            Actual  : Type '"A" | "B"' is not assignable to type '"B"'.
        */
        const v: "B" = discriminated2.value;
    }
    
    if (discriminated2.d == E.A) {
        /**
            Expected: Assignment works
            Actual  : Assignment works
        */
        const v: "A" = discriminated2.value;
    }
    if (discriminated2.d == E.B) {
        /**
            Expected: Assignment works
            Actual  : Assignment works
        */
        const v: "B" = discriminated2.value;
    }

    Playground

  3. AnyhowStep commented on Jun 9, 2019

    @AnyhowStep
    ContributorAuthor

    More notes,

    enum E {
        A = 1,
        B = 2,
        C = 3,
    }
    declare const discriminated: (
        {
            d: 1,
            value: "A",
        } |
        {
            d: 2,
            value: "B",
        }
    );
    if (discriminated.d == 1) {
        /**
            Expected: Assignment works
            Actual  : Assignment works
        */
        const v: "A" = discriminated.value;
    }
    if (discriminated.d == 2) {
        /**
            Expected: Assignment works
            Actual  : Assignment works
        */
        const v: "B" = discriminated.value;
    }
    /**
        Expected: Equality check not allowed
        Actual  : Equality check not allowed
    */
    if (discriminated.d == 3) {
    }
    
    //Equality check is allowed, but narrowing not applied?
    if (discriminated.d == E.A) {
        /**
            Expected: Assignment works
            Actual  : Type '"A" | "B"' is not assignable to type '"A"'.
        */
        const v: "A" = discriminated.value;
    }
    //Equality check is allowed, but narrowing not applied?
    if (discriminated.d == E.B) {
        /**
            Expected: Assignment works
            Actual  : Type '"A" | "B"' is not assignable to type '"B"'.
        */
        const v: "B" = discriminated.value;
    }
    /**
        Expected: Equality check not allowed
        Actual  : Equality check allowed
    */
    if (discriminated.d == E.C) {
    }

    Playground

  4. AnyhowStep commented on Jun 9, 2019

    @AnyhowStep
    ContributorAuthor

    I ended up just writing yet-another-script to generate the namespace+union enum workaround.

  5. fatcerberus commented on Jun 9, 2019

    @fatcerberus

    Yeah, long story short: numeric enum types are secretly just aliases for number.

    enum E { A = 1, B = 2, C = 3 };
    
    let e: E = E.C;
    
    e = E.A;            // this is perfectly kosher
    let n: number = e;  // questionable, but makes sense
    e = 812;            // no error. not cool, man!

    They're about as typesafe as C enums - which is to say, not typesafe at all. 😛

  6. jcalz commented on Jun 10, 2019

    @jcalz
    Contributor

    It seems that oneShouldExtendA should also be "n" and not "y". In the perfect parallel universe where bit flags never existed, E is narrower than number; E.A should be equivalent to 1 & E. (Of course that intersection doesn't even work for string enums, see #21998).

  7. AnyhowStep commented on Jun 10, 2019

    @AnyhowStep
    ContributorAuthor

    E is narrower than number. But E.A and the literal 1 should be assignable to each other (in my head anyway).

    But I wouldn't mind your expected behavior if it's just applied consistently.


    I'd like it if both enum elements and number literals narrowed numeric enum types.

    It just makes sense in my head that E.A is just an alias for the type 1 and E is just an alias for 1|2. Basically, I want the namespace-workaround to be the "real" behavior of numeric enums.

    I had a discriminated union where the http status code, represented with an enum, was the discriminant.

    I'd like it if both HttpStatusCode.NOT_FOUND and 404 can be used to narrow the union type.


    And there was also other weird behavior where I needed a lot of conditional types to get the "correct" type I was looking for.

  8. fatcerberus commented on Jun 10, 2019

    @fatcerberus

    It's not narrower in practice though, because like he said, bitfields are a thing. E.A | E.B (bitwise OR, not a type union) might yield a value not listed in the enum. That said, E.A not being a literal 1 is kind of weird.

  9. locked as resolved and limited conversation to collaborators on Oct 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    QuestionAn issue which isn't directly actionable in code

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions