Skip to content

Using enum member with spaces in type position breaks TypeScriptΒ #43171

Closed

Description

@AlCalzone

Bug Report

πŸ”Ž Search Terms

enum type spaces

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about enum

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

enum Foo {
	Bar = 1,
	"B A Z" = 2,
}

interface CB {
	(arg1: Foo.Bar, arg2: string): void;
	(arg1: Foo["B A Z"], arg2: number): void;
	//         ^^^^^^^ error here, expected no error
}

declare const cb: CB;

cb(1, "1");
cb(1, 1); // expected error, got no error
cb(2, "2"); // expected error, got no error


// Workaround with `typeof`

interface CB2 {
	(arg1: Foo.Bar, arg2: string): void;
	(arg1: typeof Foo["B A Z"], arg2: number): void;
}

declare const cb2: CB2;

cb2(1, "1");
cb2(1, 1); // expected error, got no error
cb2(2, "2"); // expected error, got no error

πŸ™ Actual behavior

Enum value Foo["B A Z"] cannot be used in a type position. Working around it with typeof results in missing errors.

Bonus points in VSCode which generates invalid syntax on auto-completion:
ezgif com-video-to-gif(1)

πŸ™‚ Expected behavior

Foo["B A Z"] should be allowed in a type position like other enum members without spaces in the name. Typechecking should continue working.

Activity

  1. RyanCavanaugh commented on Mar 9, 2021

    @RyanCavanaugh
    Member

    Foo["B A Z"] refers to a property called B A Z on a value of type Foo, but there isn't one. A value of type Foo is a number.

  2. AlCalzone commented on Mar 10, 2021

    @AlCalzone
    ContributorAuthor

    Ryan Cavanaugh (@RyanCavanaugh) so there is no way to do this

    interface CB {
    	(arg1: 1, arg2: string): void;
    	(arg1: 2, arg2: number): void;
    }

    but with enum members instead of the hardcoded number literals?

    And at least the auto-completion in VSCode is broken - should I raise a separate issue for that?

  3. RyanCavanaugh commented on Mar 10, 2021

    @RyanCavanaugh
    Member

    This can't work as hoped for due to the all-numbers-are-assignable-to-enums rule

    We can take a separate bug on the completion list, yes

  4. AlCalzone commented on Mar 10, 2021

    @AlCalzone
    ContributorAuthor

    Ryan Cavanaugh (@RyanCavanaugh) Sorry but I need to come back to this again. I must have confused something while creating the repro yesterday. If I use the enum values instead of numbers it works exactly as I'd expect:

    enum Foo {
    	Bar = 1,
    	Baz = 2,
    	"B A" = 3,
    }
    
    declare function fn(foo: Foo.Bar, arg2: number): void;
    declare function fn(foo: Foo.Baz, arg2: string): void;
    declare function fn(foo: typeof Foo["B A"], arg2: boolean): void;
    // why is this necessary ^^^^^^ ?
    
    fn(Foo.Bar, "1"); // error as expected
    fn(Foo.Baz, "1"); // no error as expected
    fn(Foo.Bar, 1); // no error as expected
    fn(Foo.Baz, 1); // error as expected
    fn(Foo["B A"], true); // no error as expected
    fn(Foo["B A"], 1); // error as expected
    
    interface CB {
    	(foo: Foo.Bar, arg2: number): void;
    	(foo: Foo.Baz, arg2: string): void;
    	(foo: typeof Foo["B A"], arg2: boolean): void;
    // why?   ^^^^^^
    }
    
    declare const cb: CB;
    cb(Foo.Bar, "1"); // error as expected
    cb(Foo.Baz, "1"); // no error as expected
    cb(Foo.Bar, 1); // no error as expected
    cb(Foo.Baz, 1); // error as expected
    cb(Foo["B A"], true); // no error as expected
    cb(Foo["B A"], 1); // error as expected

    Playground

    The only weird part about this is the stray typeof which is necessary in the third overload for a reason I don't quite understand.

  5. jcalz commented on Mar 10, 2021

    @jcalz
    Contributor

    Duplicate of #25083 and the converse of #30267.

    I'm surprised that non-identifier enum members are even allowed, since they map (in my mind) to constants exported from namespace/modules. I do see that #40679 was a fix specifically to support an issue with non-identifier enum members, so they are definitely supported.


    Types and values can reuse the same names without ambiguity because they exist in different contexts; there's no confusion (for the compiler anyway) between type D = Date and const d = Date even though the name Date refers to different things in the two cases (the instance interface vs the constructor value). And so you can write interface X { a: string } and const X = 123; without any collision.

    But while the compiler is happy with this state of affairs, it seems to thoroughly confuse human beings! Especially because class and enum declarations introduce both named values and named types which are related to each other in a consistent way. And this consistency is enough to form a pattern in the minds of developers. And then people write typeof T or obj instanceof T and get confused when the compiler complains that T is only a type and not a value. Oof, and then there's the fact that there's both a typeof runtime operator in JavaScript and a typeof type operator in TypeScript which do completely different things, and again people want to write (typeof obj === MyInterface).

    When referring to a value, Foo.bar and Foo["bar"] are equivalent, which makes sense because that's how it works in JavaScript, and if you're writing Foo.bar or Foo["bar"] in a place that expects a value, there's a good chance it's going to be emitted to JavaScript.

    But when referring to a type, there is a potential naming conflict. TS has decided that Foo.bar refers to a namespace named Foo with an exported type named bar, while Foo["bar"] refers to a type named Foo with a property key named bar. If you could write them interchangeably, then there would be an ambiguity as to which type Foo["bar"] or Foo.bar was referring to. This restriction solves the problem for the compiler, but human beings tend to get tripped up.

    In the enum here, the types Foo.Bar and typeof Foo.Bar are typeof Foo["Bar"] are equivalent, because that's how enums work. The first one means "The type named Bar in the namespace named Foo" while the latter two mean "the type of the value Foo.Bar/Foo["Bar"]". There is absolutely no way to write Foo.B A Z since "B A Z" is not an identifier, so the only way you can get this type is to write typeof Foo["B A Z"] or (typeof Foo)["B A Z"].

  6. RyanCavanaugh commented on Mar 10, 2021

    @RyanCavanaugh
    Member

    jcalz has it all correct; I don't have anything to add πŸ™‚

  7. jcalz commented on Mar 10, 2021

    @jcalz
    Contributor

    Can I quote that for my rΓ©sumΓ©/epitaph?

  8. typescript-bot commented on Mar 12, 2021

    @typescript-bot
    Contributor

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

  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

    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