Skip to content

string enum works from exported strings but not from strings exported as constΒ #59187

Closed as not planned

Description

πŸ”Ž Search Terms

enum

πŸ•— Version & Regression Information

TypeScript: 5.5.3

  • This is the behavior in every version I tried

⏯ Playground Link

Reproduction Project

πŸ’» Code

// FILE: ./case1.ts
export const Case1_ImageJpegType = 'image/jpeg'
export const Case1_ImagePngType = 'image/png'

// FILE: ./case2.ts
export const Case2_ImageJpegType = 'image/jpeg' as const
export const Case2_ImagePngType = 'image/png' as const


// FILE: ./test1.ts
import {Case1_ImageJpegType, Case1_ImagePngType} from './case1.js'

enum Case1 {
  // WORKS AS EXPECTED
  Png = Case1_ImagePngType,
  Jpeg = Case1_ImageJpegType,
}

// FILE: ./test2.ts
import {Case2_ImageJpegType, Case2_ImagePngType} from './case2.js'

enum Case2 {
  // FAILS WITH: Type 'string' is not assignable to type 'number' as required for computed enum member values.
  Png = Case2_ImagePngType,
  Jpeg = Case2_ImageJpegType,
}

πŸ™ Actual behavior

Exporting a string as const breaks string enums.

npx -p typescript@5.5.3 tsc --noEmit -p tsconfig.verbatime-false.json
// FAILS with
// Type 'string' is not assignable to type 'number' as required for computed enum member values.

πŸ™‚ Expected behavior

Exporting a string with or without as const should not break string enums.

npx -p typescript@5.5.3 tsc --noEmit -p tsconfig.verbatime-false.json
// SHOULD NOT FAIL
export const MyConst = 'MyConst'
export const MyConst = 'MyConst' as const

should behave the same when used as

import {MyConst} from './something'

enum MyEnum {
  MyType = MyConst,
}

Activity

  1. snarbies commented on Jul 8, 2024

    @snarbies

    Looks like the generated typings are different with as const.

    export declare const Case1_ImageJpegType = "image/jpeg";
    versus
    export declare const Case2_ImageJpegType: "image/jpeg";

    Exactly why, I couldn't say, but I'm guessing the problem is related to the lack of an assigned value in the latter.

  2. daniele-orlando commented on Jul 9, 2024

    @daniele-orlando
    Author

    Exactly. It is a sneaky difference in the generated .d.ts file. I don't know the exact semantic difference between the two

    export declare const A1 = 'A'
    export declare const A2: 'A'

    For mere curiosity, aside from the issue explained, I would be glad if someone could point me to a resource/documentation explaining the difference between the two syntaxes.

    At the attention of: Anders Hejlsberg (@ahejlsberg)

  3. daniele-orlando commented on Jul 9, 2024

    @daniele-orlando
    Author
  4. RyanCavanaugh commented on Jul 9, 2024

    @RyanCavanaugh
    Member
    // Widening literal type
    export declare const A1 = 'A'
    // Nonwidening literal type
    export declare const A2: 'A'

    See also https://mariusschulz.com/blog/literal-type-widening-in-typescript

    The difference is basically that a reference to the unannotated one is preferentially string but can act as "A" in context, whereas the other is always just "A". This difference needs to exist for idiomatic code like this:

    const DefaultUserName = "anonymous";
    const users = [DefaultUserName];
    // OK
    users.push("bob");

    vs here with the nonwidening behavior, you get an error:

    const DefaultUserName = "anonymous" as const;
    const users = [DefaultUserName];
    // Error
    users.push("bob");
  5. daniele-orlando commented on Jul 10, 2024

    @daniele-orlando
    Author

    Thanks Ryan Cavanaugh (@RyanCavanaugh) for the clarification about the two syntaxes.

    What I still find strange is the fact that the widened form export declare const A1 = 'A' can be used as value for an enum, while the nonwidening form export declare const A2: 'A' can't be used as enum value.

    From what I understand from your explanation and from the article literal-type-widening-in-typescript, it should be the exact opposite or at least a nonwidening form should still be a valid enum value.

  6. snarbies commented on Jul 10, 2024

    @snarbies

    I understand the rationale for the difference with respect to how it affects typing now. Is the lack of a literal value in the emit what keeps you from being able to treat it as such? Would it be possible to emit export declare const A2: 'A' = 'A' or export declare const A2 = 'A' as const instead, and if so would that solve the problem?

  7. typescript-bot commented on Jul 13, 2024

    @typescript-bot
    Contributor

    This issue has been marked as "Working as Intended" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

  8. daniele-orlando commented on Jul 15, 2024

    @daniele-orlando
    Author

    Ryan Cavanaugh (@RyanCavanaugh) please can you reopen the issue? It seams to be an issue with the enum rather than a feature.

    Given these two forms

    export const Case1_ImageJpegType = 'image/jpeg' // Valid. GOOD.
    export const Case2_ImageJpegType = 'image/jpeg' as const // Invalid. BAD
    
    enum MyEnum {
      Case1 = Case1_ImageJpegType,
      Case2 = Case2_ImageJpegType,
    }

    they should be both valid enum values. Especially the second one, which at the moment is treated as invalid and shouldn't.

  9. snarbies commented on Jul 15, 2024

    @snarbies

    Since this is "working as intended", I think it needs to be approached as a proposal to change the emit.

    Would it be possible to emit export declare const A2: 'A' = 'A' or export declare const A2 = 'A' as const instead, and if so would that solve the problem?

  10. RyanCavanaugh commented on Jul 15, 2024

    @RyanCavanaugh
    Member

    Daniele Orlando (@daniele-orlando) can you open a separate issue for that? Thanks

  11. daniele-orlando commented on Jul 18, 2024

    @daniele-orlando
    Author

    Got it. I'm going to open a proposal issue about it. Thanks for the help.

  12. locked as resolved and limited conversation to collaborators on Oct 22, 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

    Working as IntendedThe behavior described is the intended behavior; this is not a bug

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions