Skip to content

Instantly share code, notes, and snippets.

@Lexedia
Last active March 29, 2025 21:33
Show Gist options
  • Save Lexedia/a2a34122fe3cee3ab821c023847118e3 to your computer and use it in GitHub Desktop.
Save Lexedia/a2a34122fe3cee3ab821c023847118e3 to your computer and use it in GitHub Desktop.
`Option`, `Some` and `None` in Dart
/// 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