skip to content

In Dart, when should you use an enhanced enum instead of a class with static const instances, and when is neither the right fit?

level: middleimportance: should knowfreq 42%

answer

  1. closed versus open set
  2. values and byName for free
  3. no outside instantiation or subclassing
  4. same data shape for every value
  5. per-case data suggests sealed classes

basics

~20 s

Use an enhanced enum for a fixed set of same-shaped values: you get values, byName, a closed type and exhaustive switches. Keep a class of constants when the set must stay open; use a sealed hierarchy when cases carry different data.

solid answer

~40 s

Before Dart 2.17 the workaround for enums with data was a class with a private const constructor and `static const` instances. An enhanced enum does the same job better when the set is **closed**: the compiler generates `values`, `index` and `name`, `byName` and `asNameMap` work, nobody can construct a new instance or subclass the type, and `switch` over it is checked for exhaustiveness. Keep a class of constants when the set must stay **open**, such as design tokens another package may extend, or when instances need to be created at runtime, for example parsed from configuration. If the cases carry **different data per case**, such as `Shipped(trackingNumber)` versus `Cancelled(reason)`, or per-instance values rather than one fixed instance per case, neither fits and a sealed class hierarchy is the right model.

code

dart · 23 lines
dart
enum OrderStatus {
  pending('Pending'),
  shipped('Shipped'),
  delivered('Delivered');

  const OrderStatus(this.label);
  final String label;
}

// Open set: other code may add sizes, so an enum would be wrong here.
class Spacing {
  const Spacing(this.value);
  final double value;

  static const small = Spacing(8);
  static const medium = Spacing(16);
}

void main() {
  print(OrderStatus.values.map((s) => s.label).join(', '));
  const custom = Spacing(12); // allowed: the set is open by design
  print(custom.value);
}

go deeper

for a junior

Recall that enums give you values, name and index automatically, while a class of constants needs them written by hand.

for a middle

Explain the closed-versus-open difference and what the compiler enforces for enums, including exhaustive switches.

for a senior

Choose between an enum, a class of constants and a sealed hierarchy from the shape of the data, and combine an enum kind with sealed state where both are needed.

for a principal

Decide which types in a shared package should be closed, since closing a set later or opening it later is a breaking change for dependants.

## Two ways to model a fixed set of named values Before Dart 2.17, a set of named values that each carried data was usually written as a **class of constants**: ```dart class OrderStatus { const OrderStatus._(this.label); final String label; static const pending = OrderStatus._('Pending'); static const shipped = OrderStatus._('Shipped'); } ``` Since 2.17, an **enhanced enum** expresses the same thing with language support: ```dart enum OrderStatus { pending('Pending'), shipped('Shipped'); const OrderStatus(this.label); final String label; } ``` Both give you named, constant instances with fields. What differs is what the compiler knows and enforces. ## What the enum gives you that the class does not | Capability | Enhanced enum | Class of constants | |---|---|---| | list of all instances | generated `values` | hand-maintained list, easy to forget | | lookup by identifier | `values.byName`, `asNameMap` | hand-written | | position | `index` | none, unless you add it | | new instances elsewhere | impossible | possible, unless the constructor is private, and then only inside that library | | subclassing | impossible | possible, unless you add a class modifier | | exhaustive `switch` | yes | no, a `default` is needed | | `Enum` as a generic bound | yes, `T extends Enum` | no | The single most valuable line is the last-but-one: when someone adds `returned` to the enum, every exhaustive `switch` over `OrderStatus` without a default stops compiling until it handles the new case. A class of constants gives no such signal. ## When the class of constants is still right - **The set must stay open.** A design-token class whose instances other packages or themes add to cannot be an enum, because enums are closed. - **Instances are created at runtime.** Values parsed from a remote config, or built from user input, are not compile-time constants. - **You need a superclass.** Enums cannot extend anything; a class of constants can extend a shared base. - **You need custom equality.** An enum cannot override `==`; a class can, for example to treat two instances as equal by code. ## When neither is right: data that differs per case Enums and classes of constants both model **one fixed instance per case, with the same fields for every case**. Many domain states do not look like that: - `shipped` needs a tracking number and a carrier; - `cancelled` needs a reason and a timestamp; - `pending` needs nothing. Forcing that into an enum leads to nullable fields that only make sense for some values. A **sealed class hierarchy** models it directly: one subclass per case, each with its own fields and many instances, and `switch` still checks exhaustiveness. A common pattern combines both: an enum for the **kind** of status, used in filters and storage, and a sealed hierarchy for the **full state** with its per-case data. ## What the two look like to callers Callers barely notice the difference in everyday use: `OrderStatus.shipped.label` reads the same in both designs. The difference shows up at the edges: - iterating all values, which is free for the enum and manual for the class; - parsing from a string, which the enum supports through `byName` and `asNameMap`; - adding a value, which the enum turns into compile errors at every incomplete switch and the class turns into silent gaps; - using the type as a generic bound, where only the enum is an `Enum`. ## A quick decision list 1. Fixed set, same fields on every value, known at compile time: **enhanced enum**. 2. Open set, or instances created at runtime, or a superclass needed: **class with static const instances**, usually with a private constructor. 3. Different data per case, or many instances per case: **sealed class hierarchy**. ## Common mistakes - Converting every group of constants into an enum, including ones other packages must extend. - Keeping a hand-written `all` list next to a class of constants, which silently goes stale when a constant is added. - Adding nullable fields to an enum so that only some values use them; that is a sealed hierarchy trying to get out.

  • You need a status filter chip for each order state and also per-state details such as a tracking number. How do you model it?
    Use an enum for the kind of state, which drives the filter chips through `values` and can be stored by name, and a sealed class hierarchy for the full state, where `Shipped` carries a tracking number and `Cancelled` a reason. Each subclass can expose its enum kind, so filters and detailed rendering share one vocabulary.
  • Can a class of constants get exhaustive switch checking if you make its constructor private?
    No. A private constructor stops code in other libraries from creating instances, but the compiler still cannot prove that the static constants are the only instances, so a switch that lists them needs a default or wildcard case. The compiler can enumerate the complete set for enums, sealed types and `bool`, which is what makes their switches checkable.

saying these in an interview costs you the question

  • A class of static const instances gets exhaustive switch checking like an enum.
  • Enhanced enums can be extended by other packages to add values.
  • Enums are the right model even when each case needs different fields.
  • A private constructor makes a class of constants equivalent to an enum.