Dart: Dart 3 New Features
Last updated: 2026-08-26
The three major features of Dart 3 — upgrade your code from "it works" to "elegant."
1. What You Will Learn
- Records: Lightweight anonymous data aggregation, multiple return values
- Pattern Matching: destructuring / if-case / switch-pattern / exhaustive check
- Sealed Classes: restricted inheritance hierarchy + exhaustive switch guarantees
- The Trio in Action: The combination of Sealed + Pattern + Records
- Bob's Scenario: DataPipeline using Sealed classes to define data source types, Pattern matching to parse results
2. A Developer's Real Story
(1) Pain Point: Deep Type Hierarchy and Verbose if-else
Bob's DataPipeline has 3 data sources (API/File/Database), each with different configurations and connection methods. He implemented them using inheritance, but the hierarchy was deep and the code was verbose. When parsing API responses, a large number of if-else checks were needed for status codes and response structures, with 200 lines of conditional code written for just 5 response types.
(2) Dart 3 New Features Solution
Sealed Classes restrict data source types, Pattern Matching elegantly handles various responses, and Records provide lightweight multiple return values.
// Sealed class: exhaustive data source types
sealed class DataSource {}
class ApiSource extends DataSource { final String endpoint; ApiSource(this.endpoint); }
class FileSource extends DataSource { final String path; FileSource(this.path); }
// Pattern matching on sealed class - compiler guarantees all cases handled
String describe(DataSource source) => switch (source) {
ApiSource(:final endpoint) => 'API: $endpoint',
FileSource(:final path) => 'File: $path',
};
// Records: return multiple values
(String, double) parseResponse(String raw) => ('ORD-001', 1500.0);
> **Output:** Run in local DartPad or using `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly depending on the SDK version.
(3) Benefits
- Sealed Classes make type branches finite and enumerable; exhaustive switch guarantees zero omissions.
- Pattern Matching reduces verbose if-else chains to declarative switch expressions.
- Records replace small classes; multiple return values no longer require a Tuple library or a Map.
3. Records
(1) Record Basics
▶ Example
> **Output:** Run in local DartPad or using `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly depending on the SDK version.
: Creating and Using Records
void main() {
// Positional record
var order = ('ORD-001', 1500.0, 'completed');
print(order.$1); // ORD-001
print(order.$2); // 1500.0
print(order.$3); // completed
// Named record (preferred for readability)
var order2 = (id: 'ORD-002', amount: 3200.0, status: 'pending');
print(order2.id); // ORD-002
print(order2.amount); // 3200.0
print(order2.status); // pending
// Mixed positional and named
var mixed = ('ORD-003', 890.0, category: 'Electronics');
print(mixed.$1); // ORD-003
print(mixed.category); // Electronics
}
> **Output:** Run in local DartPad or using `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly depending on the SDK version.
▶ Example
> **Output:** Run in local DartPad or using `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly depending on the SDK version.
: Records as Function Return Values
// Multiple return values with records
({String id, double amount, double tax}) calculateOrder(double amount, double taxRate) {
return (
id: 'ORD-${DateTime.now().millisecondsSinceEpoch}',
amount: amount,
tax: amount * taxRate,
);
}
// Positional record for quick grouping
(String, double) parseAmount(String input) {
final parts = input.split(':');
return (parts[0], double.parse(parts[1]));
}
void main() {
final order = calculateOrder(1500.0, 0.08);
print('ID: ${order.id}, Amount: \$${order.amount}, Tax: \$${order.tax}');
final (label, value) = parseAmount('Revenue:52500.75');
print('$label: \$$value USD');
}
> **Output:** Run in local DartPad or using `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly depending on the SDK version.
| Record Feature | Syntax | Description |
|---|---|---|
| Positional fields | .$1, .$2 |
Access by position |
| Named fields | .name |
Access by name |
| Type annotation | (String, int) or ({String name, int count}) |
Explicit type |
| Equality | Value equality | Equal if field values are the same |
4. Pattern Matching
(1) Pattern Matching Types
▶ Example
> **Output:** Run in local DartPad or using `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly depending on the SDK version.
: Variable Patterns and Destructuring
void main() {
// Variable pattern - extract values
var (id, amount, status) = ('ORD-001', 1500.0, 'completed');
print('ID: $id, Amount: $amount, Status: $status');
// Record destructuring with named fields
var (:id, :amount, :status) = (id: 'ORD-002', amount: 3200.0, status: 'pending');
print('ID: $id, Amount: $amount, Status: $status');
// List destructuring
var [first, second, ...rest] = [1, 2, 3, 4, 5];
print('First: $first, Second: $second, Rest: $rest');
// Map destructuring
var {'id': orderId, 'amount': orderAmount} = {'id': 'ORD-003', 'amount': 890.0};
print('Order: $orderId, Amount: $orderAmount');
}
> **Output:** Run in local DartPad or using `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly depending on the SDK version.
▶ Example
> **Output:** Run in local DartPad or using `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly depending on the SDK version.
: if-case Pattern
void main() {
Object value = '1,500.00 USD';
// if-case pattern matching
if (value case String s when s.contains('USD')) {
print('USD value: $s');
}
// Pattern matching on record
var response = (statusCode: 200, body: '{"orders": 1500}');
if (response case (statusCode: 200, :var body)) {
print('Success response: $body');
}
// Pattern matching on list
var orders = ['ORD-001', 'ORD-002', 'ORD-003'];
if (orders case [var first, var second, ...]) {
print('First two: $first, $second');
}
}
> **Output:** Run in local DartPad or using `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly depending on the SDK version.
▶ Example
> **Output:** Run in local DartPad or using `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly depending on the SDK version.
: switch Pattern
String classifyOrder(Object value) => switch (value) {
// Type pattern
int i when i > 1000 => 'High value integer: $i',
int i => 'Low value integer: $i',
double d when d >= 1000 => 'Premium: \$${d.toStringAsFixed(2)}',
double d => 'Standard: \$${d.toStringAsFixed(2)}',
String s => 'String: $s',
List l when l.length > 100 => 'Large batch: ${l.length} items',
List l => 'Small batch: ${l.length} items',
_ => 'Unknown type',
};
// Pattern matching with records
String describeResponse((int, String) response) => switch (response) {
(200, var body) => 'OK: $body',
(404, _) => 'Not Found',
(500, var msg) => 'Server Error: $msg',
(>= 400, var msg) => 'Client Error: $msg',
_ => 'Unknown response',
};
void main() {
print(classifyOrder(1500)); // High value integer: 1500
print(classifyOrder(890.0)); // Standard: $890.00
print(classifyOrder([1, 2, 3])); // Small batch: 3 items
print(describeResponse((200, 'OK'))); // OK: OK
print(describeResponse((404, 'Missing'))); // Not Found
print(describeResponse((500, 'Crash'))); // Server Error: Crash
}
> **Output:** Run in local DartPad or using `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly depending on the SDK version.
| Pattern Type | Syntax | Usage |
|---|---|---|
| Variable pattern | var x |
Extract values |
- Type pattern |
Type x| Type check + extraction | | Constant pattern |42,'hello'| Exact match | | Relational pattern |>= 1000| Range check | | Logical pattern |a \|\| b,a && b| Combine conditions | | Guard |when condition| Additional condition | | Wildcard |_| Ignore value |
5. Sealed Classes
(1) Sealed Class Hierarchy
classDiagram
class DataSource {
<<sealed>>
}
class ApiSource {
+String endpoint
+Map headers
}
class FileSource {
+String path
+Encoding encoding
}
class DatabaseSource {
+String connectionString
}
DataSource <|-- ApiSource
DataSource <|-- FileSource
DataSource <|-- DatabaseSource
note for DataSource "exhaustive switch guaranteed"
> **Output:** Run in local DartPad or using `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly depending on the SDK version.
▶ Example
> **Output:** Run in local DartPad or using `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly depending on the SDK version.
: Sealed Class Definition and Exhaustive Matching
sealed class DataSource {
const DataSource();
}
class ApiSource extends DataSource {
final String endpoint;
final Map<String, String> headers;
const ApiSource(this.endpoint, {this.headers = const {}});
}
class FileSource extends DataSource {
final String path;
const FileSource(this.path);
}
class DatabaseSource extends DataSource {
final String connectionString;
const DatabaseSource(this.connectionString);
}
// Exhaustive switch - compiler checks all subtypes
String describeSource(DataSource source) => switch (source) {
ApiSource(:final endpoint, :final headers) =>
'API: $endpoint (${headers.length} headers)',
FileSource(:final path) =>
'File: $path',
DatabaseSource(:final connectionString) =>
'Database: $connectionString',
};
// If you add a new subtype and forget to update switch,
// the compiler will report an error!
void main() {
final sources = ``<DataSource>``[
ApiSource('https://api.example.com/orders', headers: {'Authorization': 'Bearer token'}),
FileSource('/data/orders.csv'),
DatabaseSource('postgresql://localhost:5432/orders'),
];
for (final source in sources) {
print(describeSource(source));
}
}
> **Output:** Run in local DartPad or using `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly depending on the SDK version.
| Sealed Class Feature | Description |
|---|---|
| Subclass restriction | Must be defined in the same library |
| Exhaustive check | switch must cover all subtypes |
| Compile-time guarantee | Omitted switch will cause compile error when new subclass is added |
| Cannot be instantiated | Is implicitly abstract |
6. The Trio in Action
▶ Example
> **Output:** Run in local DartPad or using `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly depending on the SDK version.
: Sealed + Pattern + Records Combination
// Sealed class for API results
sealed class ApiResult``<T>`` {
const ApiResult();
}
class Success``<T>`` extends ApiResult``<T>`` {
final T data;
final (int, String) metadata; // Record: (statusCode, message)
const Success(this.data, this.metadata);
}
class ApiError``<T>`` extends ApiResult``<T>`` {
final String message;
final int? statusCode;
const ApiError(this.message, {this.statusCode});
}
class NetworkError``<T>`` extends ApiResult``<T>`` {
final String reason;
const NetworkError(this.reason);
}
// Pattern matching with destructuring
String handleResult(ApiResult<List``<String>``> result) => switch (result) {
Success(:final data, metadata: (200, final msg)) =>
'OK ($msg): ${data.length} items loaded',
Success(:final data, metadata: (final code, _)) =>
'Loaded with status $code: ${data.length} items',
ApiError(:final message, statusCode: final code?) =>
'API Error [$code]: $message',
ApiError(:final message) =>
'API Error: $message',
NetworkError(:final reason) =>
'Network Error: $reason',
};
void main() {
final results = <ApiResult<List``<String>``>>[
Success(['ORD-001', 'ORD-002'], (200, 'OK')),
Success(['ORD-003'], (206, 'Partial Content')),
ApiError('Rate limit exceeded', statusCode: 429),
ApiError('Unknown error'),
NetworkError('Connection timeout'),
];
for (final result in results) {
print(handleResult(result));
}
}
> **Output:** Run in local DartPad or using `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly depending on the SDK version.
7. Bob's Scenario: DataPipeline Data Sources and Result Parsing
▶ Example
> **Output:** Run in local DartPad or using `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly depending on the SDK version.
: Complete Data Source Abstraction
sealed class DataSource {
const DataSource();
String get displayName;
}
class ApiSource extends DataSource {
final String endpoint;
final Duration timeout;
const ApiSource(this.endpoint, {this.timeout = const Duration(seconds: 10)});
@override
String get displayName => 'API ($endpoint)';
}
class FileSource extends DataSource {
final String path;
final String format;
const FileSource(this.path, {this.format = 'csv'});
@override
String get displayName => 'File ($path, $format)';
}
class DatabaseSource extends DataSource {
final String connectionString;
final String query;
const DatabaseSource(this.connectionString, {this.query = 'SELECT * FROM orders'});
@override
String get displayName => 'Database ($connectionString)';
}
// Processing result with Records
typedef ProcessingResult = ({int processed, int skipped, double revenue, Duration time});
ProcessingResult processDataSource(DataSource source) => switch (source) {
ApiSource(:final endpoint, :final timeout) => (
processed: 50000,
skipped: 120,
revenue: 525000.0,
time: timeout,
),
FileSource(:final path, :final format) => (
processed: 100000,
skipped: 350,
revenue: 1200000.0,
time: Duration(seconds: 5),
),
DatabaseSource(:final connectionString, :final query) => (
processed: 1200000,
skipped: 800,
revenue: 15000000.0,
time: Duration(seconds: 15),
),
};
void main() {
final sources = ``<DataSource>``[
ApiSource('https://api.example.com/orders'),
FileSource('/data/orders.csv'),
DatabaseSource('postgresql://localhost:5432/orders'),
];
print('=== DataPipeline Source Analysis ===');
for (final source in sources) {
final (:processed, :skipped, :revenue, :time) = processDataSource(source);
print('\n${source.displayName}:');
print(' Processed: ${processed} orders');
print(' Skipped: $skipped records');
print(' Revenue: \$${revenue.toStringAsFixed(2)} USD');
print(' Time: ${time.inSeconds}s');
}
}
> **Output:** Run in local DartPad or using `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly depending on the SDK version.
8. Complete Example: DataPipeline Response Processing System
// ============================================
// DataPipeline Response Processing System
// Sealed classes + Pattern matching + Records
// ============================================
// Sealed class for different response types
sealed class ApiResponse {
const ApiResponse();
}
class SuccessResponse extends ApiResponse {
final int statusCode;
final Map<String, dynamic> data;
const SuccessResponse(this.statusCode, this.data);
}
class ErrorResponse extends ApiResponse {
final int statusCode;
final String message;
final String? details;
const ErrorResponse(this.statusCode, this.message, {this.details});
}
class TimeoutResponse extends ApiResponse {
final Duration timeout;
final String endpoint;
const TimeoutResponse(this.timeout, this.endpoint);
}
class RedirectResponse extends ApiResponse {
final String newEndpoint;
final int statusCode;
const RedirectResponse(this.newEndpoint, this.statusCode);
}
// Processing outcome as a Record
typedef Outcome = ({bool success, String message, double? revenue});
// Pattern matching on all response types - exhaustive!
Outcome handleResponse(ApiResponse response) => switch (response) {
SuccessResponse(statusCode: 200, :final data) when data.containsKey('orders') => (
success: true,
message: 'Loaded ${data['orders']} orders',
revenue: (data['revenue'] as num?)?.toDouble(),
),
SuccessResponse(statusCode: 200, :final data) => (
success: true,
message: 'OK but no orders field: ${data.keys}',
revenue: null,
),
SuccessResponse(statusCode: 206, :final data) => (
success: true,
message: 'Partial data: ${data.length} fields',
revenue: null,
),
SuccessResponse(statusCode: final code) => (
success: true,
message: 'Unexpected success code: $code',
revenue: null,
),
ErrorResponse(statusCode: 429, :final message) => (
success: false,
message: 'Rate limited: $message',
revenue: null,
),
ErrorResponse(statusCode: final code, :final message, :final details?) => (
success: false,
message: 'Error [$code]: $message - $details',
revenue: null,
),
ErrorResponse(statusCode: final code, :final message) => (
success: false,
message: 'Error [$code]: $message',
revenue: null,
),
TimeoutResponse(:final timeout, :final endpoint) => (
success: false,
message: 'Timeout after ${timeout.inSeconds}s on $endpoint',
revenue: null,
),
RedirectResponse(:final newEndpoint, :final statusCode) => (
success: false,
message: 'Redirect ($statusCode) to $newEndpoint',
revenue: null,
),
};
void main() {
final responses = ``<ApiResponse>``[
SuccessResponse(200, {'orders': 50000, 'revenue': 525000.0}),
SuccessResponse(200, {'products': 3000}),
SuccessResponse(206, {'partial': true}),
ErrorResponse(429, 'Rate limit exceeded'),
ErrorResponse(500, 'Internal server error', details: 'Database connection lost'),
ErrorResponse(404, 'Not found'),
TimeoutResponse(const Duration(seconds: 30), '/api/v1/orders'),
RedirectResponse('/api/v2/orders', 301),
];
print('=== DataPipeline Response Handler ===\n');
for (final response in responses) {
final (:success, :message, :revenue) = handleResponse(response);
final status = success ? 'OK' : 'FAIL';
final rev = revenue != null ? ' (\$${revenue.toStringAsFixed(2)} USD)' : '';
print('[$status] $message$rev');
}
}
> **Output:** Run in local DartPad or using `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; results may vary slightly depending on the SDK version.
Output:
=== DataPipeline Response Handler ===
[OK] Loaded 50000 orders ($525000.00 USD)
[OK] OK but no orders field: (products)
[OK] Partial data: 1 fields
[FAIL] Rate limited: Rate limit exceeded
[FAIL] Error [500]: Internal server error - Database connection lost
[FAIL] Error [404]: Not found
[FAIL] Timeout after 30s on /api/v1/orders
[FAIL] Redirect (301) to /api/v2/orders
❓ FAQ
Q: What's the difference between Records and Classes? A: Records are anonymous, value-based lightweight data structures; Classes have names, are reference-based, and can have methods. Records are suitable for simple data passing, while Classes are for complex business logic.
Q: What's the difference between a Sealed class and an abstract class? A: Sealed classes restrict subclasses to the same library, allowing the compiler to perform exhaustive checks; abstract classes can have subclasses defined anywhere, preventing exhaustiveness. Use sealed when you need exhaustive guarantees.
Q: Where can Pattern matching be used? A: Switch expressions, switch statements, if-case, variable declarations, for-in loops. In Dart 3, patterns are everywhere.
Q: What is the performance of Records? A: Records compile to ordinary objects, with performance comparable to small classes. There is no extra overhead, but they are also not faster than classes. Choose based on readability.
Q: Can a sealed class have a constructor? A: It can have factory constructors (which return subclasses), but the class itself cannot be instantiated (it's implicitly abstract). Constructors are mainly for sharing initialization logic among subclasses.
Q: Is there a difference between
_anddefaultin a switch expression? A: Functionally, they are the same – both are catch-all matches. Dart 3 recommends using_(wildcard pattern) for conciseness.defaultis the legacy syntax retained for compatibility.
Q: Can Records be used as keys for a Map? A: Yes. Records are based on value equality and automatically implement == and hashCode.
(1, 2) == (1, 2)is true.
📖 Summary
- Records are lightweight anonymous data aggregation with support for positional and named fields, and value-based equality.
- Pattern Matching supports if-case, switch-pattern, variable destructuring, and guard conditions.
- Sealed Classes restrict subclasses to the same library; the compiler guarantees an exhaustive switch.
- The Trio in Action: Sealed defines a finite type → Pattern matches and destructures → Records provide lightweight return values.
- This is Dart 3's most core upgrade, making code safer, more concise, and more expressive.
📝 Exercises
- Basic (⭐): Define two Records: a positional record
(String, double)representing order ID and amount, and a named record({String id, double amount, String status}). Create instances and destructure/print them. - Intermediate (⭐⭐): Use a Sealed Class to define 3 payment methods (CreditCard/PayPal/BankTransfer), each with different fields. Implement a
describePayment()method using a switch expression, ensuring exhaustiveness. - Challenge (⭐⭐⭐): Design a complete API response processing system: Use a Sealed Class to define response types (Success/ValidationError/ServerError/Timeout), each carrying different Record data. Implement a response handler using Pattern Matching, incorporating relational patterns and guard conditions.