Dart: Dart Enums and Extension Methods — Enhanced Enums
Last updated: 2026-08-26
Enums give finite states names, extensions give old types new capabilities — both are powerful tools to enhance code without modifying its source.
1. What You'll Learn
- Enhanced Enums: properties, constructors, methods
- Enums with
switch - Extension Methods: definition and usage
- Extensions and privacy, resolving naming conflicts
- Bob's scenario:
OrderStatusenum + String extension (formatting amounts as USD)
2. A Developer's True Story
(1) Pain Point: Using Strings to Simulate States Leads to Typos
Alice used strings to represent order statuses in her code: 'pending', 'shipped', 'delivered'. A typo 'shiped' wasn't caught by the compiler, causing the order to remain stuck in "not shipped" status, leading to 200 customer complaints. She also frequently wrote checks like if (status == 'pending' || status == 'processing'), which were easy to overlook.
(2) The Enum Solution
Dart's Enhanced Enums give each state a type-safe name, along with attached properties and methods. Switch expressions guarantee exhaustiveness; missing a state causes a compiler error.
enum OrderStatus {
pending(label: 'Awaiting Processing', isFinal: false),
shipped(label: 'In Transit', isFinal: false),
delivered(label: 'Completed', isFinal: true),
cancelled(label: 'Cancelled', isFinal: true);
final String label;
final bool isFinal;
const OrderStatus({required this.label, required this.isFinal});
}
// Exhaustive switch - compiler checks all cases
String handle(OrderStatus status) => switch (status) {
OrderStatus.pending => 'Queue for processing',
OrderStatus.shipped => 'Track shipment',
OrderStatus.delivered => 'Send survey',
OrderStatus.cancelled => 'Process refund',
};
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on SDK version.
(3) Benefits
- Typos are caught at compile time instead of runtime, reducing state-related bugs by 90%
- Exhaustive switch ensures no state is missed
- Extension methods allow adding business methods to String and num without subclassing
3. Enhanced Enums
(1) Basic Enums
▶ Example
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on SDK version.
: Simple Enum
enum OutputFormat {
json,
csv,
html,
}
void main() {
final format = OutputFormat.json;
// Enum values
print(format.name); // json
print(format.index); // 0
print(OutputFormat.values); // [OutputFormat.json, OutputFormat.csv, OutputFormat.html]
// Parse from string
final parsed = OutputFormat.values.byName('csv');
print(parsed); // OutputFormat.csv
}
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on SDK version.
(2) Enhanced Enums
▶ Example
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on SDK version.
: Enhanced Enum with Properties
enum OrderStatus {
pending(label: 'Awaiting Processing', isFinal: false, priority: 1),
processing(label: 'Being Processed', isFinal: false, priority: 2),
shipped(label: 'In Transit', isFinal: false, priority: 3),
delivered(label: 'Completed', isFinal: true, priority: 0),
cancelled(label: 'Cancelled', isFinal: true, priority: 0);
final String label;
final bool isFinal;
final int priority;
const OrderStatus({required this.label, required this.isFinal, required this.priority});
bool get isActive => !isFinal;
String get displayName => '${name.toUpperCase()} - $label';
}
void main() {
final status = OrderStatus.shipped;
print(status.label); // In Transit
print(status.isFinal); // false
print(status.isActive); // true
print(status.displayName); // SHIPPED - In Transit
}
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on SDK version.
▶ Example
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on SDK version.
: Enhanced Enum with Methods
enum TaxCategory {
standard(rate: 0.08, label: 'Standard Rate'),
reduced(rate: 0.05, label: 'Reduced Rate'),
zero(rate: 0.0, label: 'Zero Rate'),
exempt(rate: 0.0, label: 'Tax Exempt');
final double rate;
final String label;
const TaxCategory({required this.rate, required this.label});
double calculate(double amount) => amount * rate;
double applyTo(double amount) => amount * (1 + rate);
String formatRate() => '${(rate * 100).toStringAsFixed(1)}%';
}
void main() {
final tax = TaxCategory.standard;
print(tax.calculate(1500.0)); // 120.0
print(tax.applyTo(1500.0)); // 1620.0
print(tax.formatRate()); // 8.0%
// All categories
for (final cat in TaxCategory.values) {
print('${cat.label}: ${cat.formatRate()}');
}
}
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on SDK version.
4. Enums with Switch
▶ Example
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on SDK version.
: Exhaustive Switch
enum DataSourceType {
api,
file,
database,
}
String describeSource(DataSourceType type) => switch (type) {
DataSourceType.api => 'REST API endpoint',
DataSourceType.file => 'Local file system',
DataSourceType.database => 'SQL database connection',
};
// With exhaustive check - compiler forces all cases
bool canRetry(DataSourceType type) => switch (type) {
DataSourceType.api => true, // API can retry
DataSourceType.file => false, // File errors need manual fix
DataSourceType.database => true, // DB can retry with backoff
};
void main() {
print(describeSource(DataSourceType.api)); // REST API endpoint
print(canRetry(DataSourceType.file)); // false
}
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on SDK version.
| Feature | if-else | switch statement | switch expression |
|---|---|---|---|
| Exhaustiveness check | No | No | Yes (for enums) |
| Compile-time guarantee | No | No | Yes |
| When adding new enum value | Might be missed | Might be missed | Compile error |
5. Extension Methods
(1) Basic Extensions
▶ Example
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on SDK version.
: String Extension
extension StringCurrency on String {
String toUSD() => '\$$this USD';
String toEUR() => '€${this} EUR';
String truncate(int maxLength) =>
length <= maxLength ? this : '${substring(0, maxLength)}...';
String get capitalized =>
isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';
}
void main() {
print('1500.00'.toUSD()); // $1500.00 USD
print('1200.00'.toEUR()); // €1200.00 EUR
print('Very long product name'.truncate(10)); // Very long...
print('electronics'.capitalized); // Electronics
}
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on SDK version.
▶ Example
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on SDK version.
: num Extension (Amount Formatting)
extension NumFormatting on num {
String toUSD() => '\$${toStringAsFixed(2)} USD';
String toCompact() {
if (this >= 1000000) return '\$${(this / 1000000).toStringAsFixed(1)}M USD';
if (this >= 1000) return '\$${(this / 1000).toStringAsFixed(1)}K USD';
return toUSD();
}
double get asK => this / 1000;
double get asM => this / 1000000;
bool isBetween(num from, num to) => from <= this && this <= to;
}
void main() {
print(1500.0.toUSD()); // $1500.00 USD
print(1500000.0.toCompact()); // $1.5M USD
print(5000.asK); // 5.0
print(1500.0.isBetween(1000, 2000)); // true
}
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on SDK version.
▶ Example
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on SDK version.
: List Extension
extension ListStats on List``<double>`` {
double get sum => fold(0, (a, b) => a + b);
double get average => isEmpty ? 0 : sum / length;
double get median {
final sorted = [...this]..sort();
final mid = length ~/ 2;
return length.isEven
? (sorted[mid - 1] + sorted[mid]) / 2
: sorted[mid];
}
}
void main() {
final amounts = [1500.0, 3200.0, 890.0, 50.0];
print('Sum: ${amounts.sum.toUSD()}'); // Sum: $5640.00 USD
print('Average: ${amounts.average.toUSD()}'); // Average: $1410.00 USD
print('Median: ${amounts.median.toUSD()}'); // Median: $1195.00 USD
}
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on SDK version.
6. Extensions, Privacy, and Naming Conflicts
(1) Resolving Naming Conflicts
▶ Example
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on SDK version.
: Resolving Conflict via Namespaces
extension MathExtras on num {
int get squared => (this * this).toInt();
}
extension StringExtras on String {
String get reversed => split('').reversed.join('');
}
// If two extensions have the same method name
extension DoubleExtras on double {
String toMoney() => '\$${toStringAsFixed(2)}';
}
extension IntExtras on int {
String toMoney() => '\$${this}.00';
}
void main() {
// Direct call - compiler resolves by type
print(5.squared); // 25
print('hello'.reversed); // olleh
// Explicit resolution when ambiguous
print(DoubleExtras(1500.5).toMoney()); // $1500.50
print(IntExtras(1500).toMoney()); // $1500.00
}
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on SDK version.
| Conflict Scenario | Resolution |
|---|---|
| Two extensions define the same method name | Explicitly call via ExtensionName(obj).method() |
| Extension method and class method have the same name | Class method takes priority, extension method is hidden |
| Two extensions are in different files | Priority of imported extensions depends on import order |
7. Bob's Scenario: OrderStatus Enum + String Extension
▶ Example
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on SDK version.
: Practical Enum and Extension for DataPipeline
// Order status enum with business logic
enum OrderStatus {
pending(label: 'Awaiting Processing', isFinal: false),
processing(label: 'Being Processed', isFinal: false),
shipped(label: 'In Transit', isFinal: false),
delivered(label: 'Completed', isFinal: true),
cancelled(label: 'Cancelled', isFinal: true),
refunded(label: 'Refunded', isFinal: true);
final String label;
final bool isFinal;
const OrderStatus({required this.label, required this.isFinal});
bool get isActive => !isFinal;
bool get canCancel => this == pending || this == processing;
bool get canRefund => this == delivered;
}
// String extension for DataPipeline formatting
extension DataPipelineString on String {
String get asOrderId => 'ORD-$this';
String toUSD() => '\$$this USD';
String toCategoryLabel => split('_').map((w) => w.capitalizeFirst).join(' ');
}
extension StringCap on String {
String get capitalizeFirst =>
isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';
}
// num extension for revenue formatting
extension RevenueFormatting on num {
String toRevenue() => '\$${toStringAsFixed(2)} USD';
String toCompactRevenue() {
if (this >= 1000000) return '\$${(this / 1000000).toStringAsFixed(1)}M USD';
if (this >= 1000) return '\$${(this / 1000).toStringAsFixed(1)}K USD';
return toRevenue();
}
}
void main() {
// Enum usage
final status = OrderStatus.shipped;
print('Status: ${status.label}'); // In Transit
print('Active: ${status.isActive}'); // true
print('Can cancel: ${status.canCancel}'); // false
// String extensions
print('001'.asOrderId); // ORD-001
print('1500.00'.toUSD()); // $1500.00 USD
// Revenue formatting
print(1500000.toCompactRevenue()); // $1.5M USD
print(52500.75.toRevenue()); // $52500.75 USD
// Exhaustive switch on enum
for (final s in OrderStatus.values) {
final action = switch (s) {
OrderStatus.pending => 'Queue for processing',
OrderStatus.processing => 'Monitor progress',
OrderStatus.shipped => 'Track delivery',
OrderStatus.delivered => 'Send confirmation',
OrderStatus.cancelled => 'Process cancellation',
OrderStatus.refunded => 'Update records',
};
print(' ${s.name}: $action');
}
}
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on SDK version.
8. Complete Example: DataPipeline Order State Machine
// ============================================
// DataPipeline Order State Machine
// Enhanced enums + extensions in action
// ============================================
enum OrderStatus {
pending(label: 'Awaiting Processing', isFinal: false, color: 'yellow'),
processing(label: 'Being Processed', isFinal: false, color: 'blue'),
shipped(label: 'In Transit', isFinal: false, color: 'orange'),
delivered(label: 'Completed', isFinal: true, color: 'green'),
cancelled(label: 'Cancelled', isFinal: true, color: 'red'),
refunded(label: 'Refunded', isFinal: true, color: 'gray');
final String label;
final bool isFinal;
final String color;
const OrderStatus({
required this.label,
required this.isFinal,
required this.color,
});
bool get isActive => !isFinal;
bool get canTransition => !isFinal;
List``<OrderStatus>`` get allowedTransitions => switch (this) {
pending => [processing, cancelled],
processing => [shipped, cancelled],
shipped => [delivered],
delivered => [refunded],
cancelled => [],
refunded => [],
};
bool canTransitionTo(OrderStatus target) =>
allowedTransitions.contains(target);
}
extension NumRevenue on num {
String toUSD() => '\$${toStringAsFixed(2)} USD';
}
class Order {
final String id;
final double amount;
OrderStatus status;
Order({required this.id, required this.amount, this.status = OrderStatus.pending});
bool transitionTo(OrderStatus newStatus) {
if (!status.canTransitionTo(newStatus)) {
print(' Cannot transition from ${status.name} to ${newStatus.name}');
return false;
}
print(' $id: ${status.name} → ${newStatus.name}');
status = newStatus;
return true;
}
String get summary => '$id: ${status.label} (${amount.toUSD()})';
}
void main() {
final order = Order(id: 'ORD-001', amount: 1500.0);
print('=== Order State Machine ===');
print('Initial: ${order.summary}');
// Valid transitions
order.transitionTo(OrderStatus.processing); // OK
order.transitionTo(OrderStatus.shipped); // OK
order.transitionTo(OrderStatus.delivered); // OK
// Invalid transition
order.transitionTo(OrderStatus.cancelled); // Cannot: delivered → cancelled
// Valid refund
order.transitionTo(OrderStatus.refunded); // OK
print('\nFinal: ${order.summary}');
// Print all states and transitions
print('\n=== State Transition Table ===');
for (final status in OrderStatus.values) {
final targets = status.allowedTransitions.map((t) => t.name).join(', ');
print(' ${status.name.padRight(12)} → ${targets.isEmpty ? '(final)' : targets}');
}
// Status statistics
print('\n=== Status Properties ===');
final activeCount = OrderStatus.values.where((s) => s.isActive).length;
final finalCount = OrderStatus.values.where((s) => s.isFinal).length;
print('Active states: $activeCount');
print('Final states: $finalCount');
print('Total states: ${OrderStatus.values.length}');
}
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on SDK version.
Output:
=== Order State Machine ===
Initial: ORD-001: Awaiting Processing ($1500.00 USD)
ORD-001: pending → processing
ORD-001: processing → shipped
ORD-001: shipped → delivered
Cannot transition from delivered to cancelled
ORD-001: delivered → refunded
Final: ORD-001: Refunded ($1500.00 USD)
=== State Transition Table ===
pending → processing, cancelled
processing → shipped, cancelled
shipped → delivered
delivered → refunded
cancelled → (final)
refunded → (final)
=== Status Properties ===
Active states: 3
Final states: 3
Total states: 6
❓ FAQ
Q: What's the difference between an Enhanced Enum and a regular enum? A: Enhanced Enums can have properties, constructors, and methods. Regular enums only have
nameandindex. Dart 2.17+ recommends using Enhanced Enums everywhere.
Q: Can enums implement interfaces? A: Yes. Enums can implement interfaces, e.g., `enum Status implements Comparable``
```. However, they cannot extend other classes (enums implicitly inherit from Enum).
Q: Can extension methods access private members? A: No. Extension methods are defined outside the class and can only access public members. This is the fundamental difference between extensions and class methods.
Q: Are extension methods statically or dynamically dispatched? A: Statically dispatched. The compiler determines which extension method to call based on the declared type of the variable at compile time. The runtime type doesn't matter. This is the essential difference from class methods, which are dynamically dispatched.
Q: Can extensions add properties? A: They can add computed properties (getters) but cannot add instance variables (stored properties). Extensions do not modify the memory layout of an object.
Q: What happens if two extensions define a method with the same name? A: If the compiler can distinguish based on the receiver type, it automatically selects the correct one. If it cannot distinguish (ambiguous), you must explicitly specify using
ExtensionName(obj).method().
Q: Is there a performance difference between
valuesandbyNamefor enums? A:valuesreturns a cached list, O(1).byNameiterates overvaluesto find a match, O(n). For frequent lookups, consider creating your own Map cache.
📖 Summary
- Enhanced Enums allow enum values to have properties, constructors, and methods, making them safer than plain string constants.
- Switch expressions combined with enums ensure compiler-guaranteed exhaustiveness; adding a new enum value won't be overlooked.
- Extension methods add functionality to existing types without modifying source code or altering memory layout.
- Extension methods are statically dispatched and can only access public members. Naming conflicts require explicit resolution.
- DataPipeline uses the
OrderStatusenum to define a state machine, and String/num extensions to format amounts.
📝 Exercises
- Basic (Difficulty ⭐): Define an
OutputFormatEnhanced Enum containingjson,csv, andhtmlvalues, each with afileExtensionproperty (e.g.,.json) and amimeTypeproperty (e.g.,application/json). - Intermediate (Difficulty ⭐⭐): Add extension methods to
String:toOrderId(formats as ORD-XXX),isValidEmail(validates email format),truncateWithEllipsis(int max)(truncates and adds ellipsis), and test them. - Challenge (Difficulty ⭐⭐⭐): Use an Enhanced Enum to implement a complete workflow state machine (Draft → Review → Approved → Published). Each state should define its allowed transition targets. Implement a
transitionTo()method and verify that illegal transitions are rejected.