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


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.

DART
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',
};
TEXT 📖 Display only
> **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


3. Enhanced Enums

(1) Basic Enums

▶ Example

TEXT 📖 Display only
> **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

DART
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
}
TEXT 📖 Display only
> **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

TEXT 📖 Display only
> **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

DART
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
}
TEXT 📖 Display only
> **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

TEXT 📖 Display only
> **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

DART
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()}');
  }
}
TEXT 📖 Display only
> **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

TEXT 📖 Display only
> **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

DART
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
}
TEXT 📖 Display only
> **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

TEXT 📖 Display only
> **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

DART
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
}
TEXT 📖 Display only
> **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

TEXT 📖 Display only
> **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)

DART
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
}
TEXT 📖 Display only
> **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

TEXT 📖 Display only
> **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

DART
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
}
TEXT 📖 Display only
> **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

TEXT 📖 Display only
> **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

DART
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
}
TEXT 📖 Display only
> **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

TEXT 📖 Display only
> **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

DART
// 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');
  }
}
TEXT 📖 Display only
> **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

DART
// ============================================
// 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}');
}
TEXT 📖 Display only
> **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:

TEXT 📖 Display only
=== 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 name and index. 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 values and byName for enums? A: values returns a cached list, O(1). byName iterates over values to find a match, O(n). For frequent lookups, consider creating your own Map cache.


📖 Summary


📝 Exercises

  1. Basic (Difficulty ⭐): Define an OutputFormat Enhanced Enum containing json, csv, and html values, each with a fileExtension property (e.g., .json) and a mimeType property (e.g., application/json).
  2. 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.
  3. 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.

← Previous Lesson | Next Lesson →

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏