Is it possible to declare a Map type that requires all enum values to be present as keys?

Viewed 51

How can I require the Dart compiler to warn me when I forget to include all members of an enum in a map? For example, in the following:

enum Size {
  small,
  medium,
  large,
}

// This is a valid Dart code. Dart compiler doesn't require `Size.large` to be present.
final Map<Size, ButtonSize> sizeMap = {
  Size.small: const MyClass(),
  Size.medium: const MyClass(),
};

The Dart compiler isn't that restrictive. It doesn't require all enum values to be present in the Map, so I can't be sure that the following code will return an instance of MyClass. It might resolve to null:

final MyClass instance = sizeMap[Size.small]; // unsafe

I have to either do this:

final MyClass? instance = sizeMap[Size.small]; // `instance` might be `null`

or this:

final MyClass instance = sizeMap[Size.small] as MyClass; // `instance` might still be `null`, but we're pretending it's not.

Both solutions are far from perfect. The first one implies further null checks in the code, the second one smells because of typecasting.

Is there any way to declare the type of sizeMap so that all enum values must be present?

1 Answers

Instead of using a Map, you'd be better off with a function and a switch statement:

MyClass mapSize(Size size) {
  switch (size) {
    case Size.small:
      return const MyClass();
    case Size.medium:
      return const MyClass();
  }
}

the above code should generate a warning that not all enum values are handled.

If you want a Map because you want to allow it to be mutated, you could provide separate functions to set and get values:

var _smallValue = const MyClass();
var _mediumValue = const MyClass();
var _largeValue = const MyClass();

MyClass mapSize(Size size) {
  switch (size) {
    case Size.small:
      return _smallValue;
    case Size.medium:
      return _mediumValue;
    case Size.large:
      return _largeValue;
  }
}

void setMappedSize(Size size, MyClass value) {
  switch (size) {
    case Size.small:
      _smallValue = value;
      break;
    case Size.medium:
      _mediumValue = value;
      break;
    case Size.large:
      _largeValue = value;
      break;
  }
}

If you find that to be too verbose, and you just want callers to avoid returning null, you could wrap a Map internally:

const _defaultValue = const MyClass();
final _sizeMap = <Size, MyClass>{};

MyClass mapSize(Size size) => _sizeMap[size] ?? _defaultValue;

void setMappedSize(Size size, MyClass value) => _sizeMap[size] = value;
Related