Type-safe enums for limited values

Viewed 131

How does one achieve type-safe enums over a limited range of values in Go?

For example, suppose I wanted to model t-shirts with a simple Size and Color, both with limited possible values, ensuring that I could not create an instance with unsupported values.

I could declare the type Size and Color based on string, define enums for the valid values, and a TShirt type that uses them:

type Size string
const (
  Small  Size = "sm"
  Medium      = "md"
  Large       = "lg"
  // ...
)

type Color string
const (
  Red   Color = "r"
  Green       = "g"
  Blue        = "b"
  // ...
)

type TShirt struct {
  Size  Size
  Color Color
}

var mediumBlueShirt = TShirt{Medium, Blue}

But how could I ensure that no TShirt with undefined size/color is created?

var doNotWant = TShirt{"OutrageouslyLarge", "ImpossibleColor"}
2 Answers

If you make your enums based on integer, you can create a pseudo value which can be used to check validity:

type Size int
const (
    Small Size = iota
    Medium
    Large
    maxSize // Always keep this at the end
)

var sizeToString = map[Size]string{
    Small: "sm",
    Medium: "md",
    Large: "lg",
}

func (s Size) String() string {
    return sizeToString[s]
}

func (s Size) Valid() bool {
    return s > 0 && s < maxSize
}

If you keep the maxSize at the end of the const block your Valid function will always work, even after adding or removing enum values.

You can do the same for your second enum type and your composite type can also define a Valid functions which returns true if both enums are valid as well.

When I use this pattern I always include a unit test to make sure that every enum value has a string translation so I don't forget. You can do this by making a for loop from 0 to maxSize and check that you never get an empty string back from sizeToString

There is no direct support for limited set enum, however you can use the type system to achieve that:

// Exported interface 
type Size interface {
   // Unexported method
   isSize()
}

// unexported type
type size struct {
   sz string
}

func (s size) String() string {return s.sz}
// Implement the exported interface
func (size) isSize() {}

var (
  Small Size = size{"small"}
  Large Size = size{"large"}
)

This way you can only use predeclared values.

This is usually not worth the trouble. Consider validating such enums against a known valueset.

Related