Skip to content

Proposal: Allow Same Names Across Different Enum Types

xushiwei edited this page Jun 23, 2026 · 9 revisions

Extend the Simplified Enum Type Declaration feature to permit multiple enum types to share identical underlying constant values when they use the same constant name, instead of treating this as a redeclaration error.

Background

XGo's enum type declaration uses the following syntax:

type Identifier const (
    Name1 [= Expr1]
    Name2 [= Expr2]
    ...
)
  • Identifier becomes a new named type.
  • Each Name becomes a package-level constant of type Identifier.
  • Expr follows the same rules as ordinary const declarations, including iota support and the "repeat previous expression" shorthand.

For example:

type Weekday const (
    Sunday = iota
    Monday
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
)

This expands to underlying type int and generates:

type Weekday int

const (
    Sunday    Weekday = iota
    Monday
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
)

Because each enum constant is a package-level identifier, it is accessed directly by nameSunday, Monday, etc. — not via a type-qualified form like Weekday.Sunday.

Motivation

Because enum constants are package-level identifiers, two enum types in the same package that both declare a constant with the same name will currently produce a redeclaration error. In practice, however, it is very common for different enum types to independently use the same sentinel name — most typically None — with identical underlying values. For example:

type RepeatMode const (
    None    = ""
    Once    = "Once"
    Daily   = "Daily"
    Weekly  = "Weekly"
    Monthly = "Monthly"
    Yearly  = "Yearly"
)

type MonthlyRepeatOnDay const (
    None       = ""
    Sunday     = "Sunday"
    Monday     = "Monday"
    Tuesday    = "Tuesday"
    Wednesday  = "Wednesday"
    Thursday   = "Thursday"
    Friday     = "Friday"
    Saturday   = "Saturday"
    Day        = "day"
    Weekday    = "weekday"
    WeekendDay = "weekend day"
)

Both types declare None = "". This is intentional: each type independently models the concept of "not set" with the same natural sentinel. The current prohibition forces awkward workarounds — distinct sentinel strings, extra wrapper types, or giving up enum types entirely — all of which compromise readability and domain correctness.

Proposal

Rule

Two or more enum type declarations in the same package may declare constants with the same name, subject to the following rules:

  1. If a name N is declared in multiple enum types and all declarations assign the same constant value V, the declarations are merged: N is emitted once as an untyped constant with value V, and each enum type gains a typed alias for N.
  2. If a name N is declared in multiple enum types but with different underlying values, it is a compile-time error.
  3. Within a single enum type, all constant names must remain unique.
  4. The shared constant's value must be of kind int or string. Values of any other kind (e.g. float64, complex128, bool) are not permitted as shared enum constants and result in a compile-time error.

Legality examples

LegalNone = "" appears in both types with the same value:

type RepeatMode const (
    None    = ""
    Once    = "Once"
    ...
)

type MonthlyRepeatOnDay const (
    None   = ""
    Sunday = "Sunday"
    ...
)

IllegalNone appears in both types but with different values:

type RepeatMode const (
    None = "none"
)

type MonthlyRepeatOnDay const (
    None = "no repeat"  // error: "None" already declared with value "none"
)

Illegal — the shared constant's value is not of kind int or string:

type Foo const (
    None = false   // bool — cannot be shared
    Bar  = true
)

type Baz const (
    None = false   // error: shared constant "None" must be of kind int or string
    Qux  = true
)

Generated Go Code

Shared-name case

The XGo source:

type RepeatMode const (
    None    = ""
    Once    = "Once"
    Daily   = "Daily"
    Weekly  = "Weekly"
    Monthly = "Monthly"
    Yearly  = "Yearly"
)

type MonthlyRepeatOnDay const (
    None       = ""
    Sunday     = "Sunday"
    Monday     = "Monday"
    Tuesday    = "Tuesday"
    Wednesday  = "Wednesday"
    Thursday   = "Thursday"
    Friday     = "Friday"
    Saturday   = "Saturday"
    Day        = "day"
    Weekday    = "weekday"
    WeekendDay = "weekend day"
)

generates:

type RepeatMode string
type MonthlyRepeatOnDay string

const None = ""  // untyped string constant; usable as both RepeatMode and MonthlyRepeatOnDay

const (
    _None_1 RepeatMode = ""
    Once    RepeatMode = "Once"
    Daily   RepeatMode = "Daily"
    Weekly  RepeatMode = "Weekly"
    Monthly RepeatMode = "Monthly"
    Yearly  RepeatMode = "Yearly"
)

const (
    _None_2    MonthlyRepeatOnDay = ""
    Sunday     MonthlyRepeatOnDay = "Sunday"
    Monday     MonthlyRepeatOnDay = "Monday"
    Tuesday    MonthlyRepeatOnDay = "Tuesday"
    Wednesday  MonthlyRepeatOnDay = "Wednesday"
    Thursday   MonthlyRepeatOnDay = "Thursday"
    Friday     MonthlyRepeatOnDay = "Friday"
    Saturday   MonthlyRepeatOnDay = "Saturday"
    Day        MonthlyRepeatOnDay = "day"
    Weekday    MonthlyRepeatOnDay = "weekday"
    WeekendDay MonthlyRepeatOnDay = "weekend day"
)

The shared name None is emitted as a single untyped string constant. Each participating enum type also gets a renamed typed constant (_None_1, _None_2, …) at the original position within its own const block, allowing reflection-based tooling (e.g. auto-generated String() methods) to associate the value with the name "None" inside each type's value set. The renamed constants are unexported and not intended for direct use.

Integer enum with shared sentinel (iota-based)

The shared name itself may also be iota-based, as long as it evaluates to the same int value in all participating types. When None is the first entry in each enum, it evaluates to 0 in both, so sharing is valid. The two types may differ in their explicit underlying type annotation (e.g. uint8(iota) vs plain iota):

type Direction const (
    None  = uint8(iota)  // 0
    North                // 1
    South                // 2
    East                 // 3
    West                 // 4
)

type Priority const (
    None = iota  // 0
    Low          // 1
    High         // 2
)

var d Direction = None
var p Priority = None

echo d, p

generates:

type Direction uint8
type Priority int

const None = 0  // untyped integer constant; assignable to both Direction and Priority

const (
    _None_1 Direction = uint8(iota)  // 0
    North                            // 1
    South                            // 2
    East                             // 3
    West                             // 4
)

const (
    _None_2 Priority = iota  // 0
    Low                      // 1
    High                     // 2
)

var d Direction = None
var p Priority = None

fmt.Println(d, p)

Semantics

Type safety

None being emitted as an untyped constant means it is assignable to any enum type whose underlying type is compatible, just like an untyped numeric or string literal. However, the two enum types themselves remain distinct named types and are not mutually assignable:

var r RepeatMode        = None  // OK: untyped "" is assignable to RepeatMode
var m MonthlyRepeatOnDay = None  // OK: untyped "" is assignable to MonthlyRepeatOnDay
r = m                            // compile error: cannot use MonthlyRepeatOnDay as RepeatMode

Name resolution

None is a single package-level identifier. There is no ambiguity: it resolves to the untyped constant "" (or 0, etc.) regardless of which enum type is expected in context. Type checking then validates the assignment against the target type as usual.

Open Questions

  1. String() / value-to-name converters. If XGo auto-generates String() methods or reverse-lookup tables for enum types, each type's converter should return its own type-local name ("None") regardless of the shared underlying value. This is straightforward since converters are per-type.

  2. Cross-package sharing. This proposal covers same-package declarations only. Enum constants from another package are accessed via the package qualifier (pkg.None) and do not interact with local declarations.

Conclusion

Allowing multiple enum types to share the same constant name when the underlying values are identical removes a common friction point in domain modeling. The rule is minimal: same name + same value + value kind must be int or string; any deviation is a compile-time error. The implementation is straightforward — emit the shared name as a single untyped constant — and full type safety is preserved because the enum types themselves remain distinct named types.

Clone this wiki locally