Last active
March 29, 2025 21:33
-
-
Save Lexedia/a2a34122fe3cee3ab821c023847118e3 to your computer and use it in GitHub Desktop.
`Option`, `Some` and `None` in Dart
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// An [Error] thrown when [Option.unwrap] fails. | |
/// | |
/// This should (in theory) not be caught, if you want to safely [Option.unwrap], use [Option.unwrapOr] or [Option.unwrapOrElse]. | |
final class UnwrapError<T> extends Error { | |
UnwrapError._(); | |
@override | |
String toString() => 'Failed to unwrap None to $T'; | |
} | |
/// An [Error] thrown when [Option.expect] fails. | |
/// | |
/// This should (in theory) not be caught. | |
final class ExpectError extends Error { | |
final String _message; | |
ExpectError._(this._message); | |
@override | |
String toString() => _message; | |
} | |
/// Represents an [Option]al value that is either [Some] if it contains a value, or [None] if it doesn't contain one. | |
/// | |
/// The [Option] class is an abstraction that allows you to represent values that may or may not be present. | |
/// It helps to avoid the pitfalls of using `null` to indicate the absence of a value, which can lead to ambiguity | |
/// and potential runtime errors. | |
/// By using [Option], you can clearly differentiate between a value that is intentionally absent and a value that is present. | |
/// | |
/// See this example: | |
/// ```dart | |
/// Future<void> updateUser({String? name, Uint8List? avatar}) async { | |
/// if (name != null) { | |
/// await updateUserName(name); | |
/// } | |
/// | |
/// if (avatar != null) { | |
/// await updateUserAvatar(avatar); | |
/// } | |
/// } | |
/// | |
/// void main() async { | |
/// await updateUser(avatar: null); | |
/// } | |
/// ``` | |
/// | |
/// When setting the `avatar` parameter explicitely to `null`, indicating the user want to delete it, this will not be ran because of the if clause guard. | |
/// Using [Option] can solve this issue: | |
/// ```dart | |
/// Future<void> updateUser({String? name, Option<Uint8List?> avatar = const None()}) async { | |
/// if (name != null) { | |
/// await updateUserName(name); | |
/// } | |
/// | |
/// if (avatar.isSome) { | |
/// await updateUserAvatar(avatar.unwrap()); | |
/// } | |
/// } | |
/// | |
/// void main() async { | |
/// await updateUser(avatar: const Some(null)); | |
/// } | |
/// ``` | |
sealed class Option<T> { | |
const Option._(); | |
/// Returns `true` if the [Option] is [Some] value. | |
bool get isSome => this is Some<T>; | |
/// Returns `true` if the [Option] is [Some] and matches [predicate]. | |
bool isSomeAnd(bool Function(T value) predicate) => switch (this) { | |
Some(:final value) => predicate(value), | |
None() => false, | |
}; | |
/// Returns `true` if the [Option] is [None]. | |
bool get isNone => this is None; | |
/// Returns `true` if the [Option] is [None] or the value inside it matches [predicate]. | |
bool isNoneOr(bool Function(T value) predicate) => switch (this) { | |
Some(:final value) => predicate(value), | |
None() => true, | |
}; | |
/// Returns the contained [Some] value or throws an [ExpectError] if it is [None]. | |
T expect(String message) => switch (this) { | |
Some(:final value) => value, | |
None() => throw ExpectError._(message), | |
}; | |
/// Unwrap this [Option] to return it's [Some] value or throw an [UnwrapError]. | |
T unwrap() => switch (this) { | |
Some(:final value) => value, | |
None() => throw UnwrapError<T>._(), | |
}; | |
/// [unwrap]s this [Option] to return it's [Some] value or fallback to [defaultValue] is this is [None]. | |
T unwrapOr(T defaultValue) => switch (this) { | |
Some(:final value) => value, | |
None() => defaultValue, | |
}; | |
/// [unwrap]s this [Option] ti return it's [Some] value or call the [f] if this is [None]. | |
T unwrapOrElse(T Function() f) => switch (this) { | |
Some(:final value) => value, | |
None() => f(), | |
}; | |
} | |
/// Represents a [Some] value of type [T]. | |
/// | |
/// The [Some] class is a concrete implementation of the [Option] type that | |
/// encapsulates a value of type [T]. | |
/// | |
/// Use the [Some] class when you want to represent a value that is guaranteed | |
/// to be present. This is particularly useful in scenarios where you want to | |
/// differentiate between a value that is explicitly set to `null` and a value | |
/// that is simply not present. | |
/// | |
/// Example: | |
/// ```dart | |
/// final Option<int> someValue = Some(42); | |
/// if (someValue.isSome) { | |
/// print('The value is: ${someValue.unwrap()}'); // Outputs: The value is: 42 | |
/// } | |
/// ``` | |
/// | |
/// The [value] property holds the actual value of type [T]. | |
final class Some<T> extends Option<T> { | |
const Some(this.value) : super._(); | |
/// The value held by this [Some] instance. | |
final T value; | |
} | |
/// No value. | |
/// | |
/// The [None] class is defined with a generic type parameter [T] to ensure compatibility with methods like | |
/// [Option.unwrapOr] and [Option.unwrapOrElse]. If we were to use `Never` as the type parameter, it would | |
/// lead to type errors because `Never` has no subtypes (unless itself), meaning it cannot represent any actual value. | |
/// Therefore, we use [T] to allow [None] to be used with any type, even though semantically it may seem | |
/// counterintuitive. | |
final class None<T> extends Option<T> { | |
const None() : super._(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment