Provides a new class (None<T>) that masquarades as a Future<T> to enable piggybacking on the only Union Type available in dart: FutureOr<T>. You can then differentiate between an explicit null value and a default None<T> value.

Unfortunately, the dart language lacks true Union Types and does not allow for extending the capabilities of FutureOr<T> so this package deploys some workarounds to use the NoneOr<T>? typedef as a standin paramter for a value that may be T, null, or None (which is a new class that implements Future but is in no way indended to actually behave like a future)


Big thank you to our sponsor Scrapp Inc for supporting the development of this package!


Features

This package introduces a new class None<T> which is a valid implementor of Future<T> enabling it to be used in place of an argument or variable of type FutureOr<T>. Included in the None type is the fallback static method that can return T? taking from the primary field when primary is of type T? or is null. However, it can return the optional fallback parameter if primary is of type None<T> or Future<T>.


Getting started

Add the package to your pubspec.yaml:

none_or: latest

Import the package with:

import 'package:none_or/none_or.dart';

See below for usage examples.


Usage

The expected utility of this package is in allowing for differentiation between explitly null values and values that are simply omitted. The below example demonstrates how NoneOr<T> could be used to level-up a copyWith function for a class with nullable fields:

import 'package:none_or/none_or.dart';
import 'package:flutter/foundation.dart';

// ...

/// A simple example immutable class with a copyWith function that uses
/// NoneOr<T> to ignore implicit no-change values given to a copyWith, while
/// respecting explicitly provided values including `null`
@immutable
class _NoneOrExampleClass {
  final bool nonNullableField;
  final bool? nullableField;
  // ...

  const _NoneOrExampleClass({
    required this.nonNullableField,
    this.nullableField,
    // ...
  });

  @override
  String toString() => 'Example($nonNullableField, $nullableField)';

  /// Creates a copy of [this] with the given fields changed
  _NoneOrExampleClass copyWith({
    bool? nonNullableField,
    NoneOr<bool>? nullableField = const None(),
    // ...
  }) =>
      _NoneOrExampleClass(
        nonNullableField: nonNullableField ?? this.nonNullableField,
        nullableField: None.fallback(nullableField, this.nullableField),
        // ...
      );
  
  /// Creates a copy of [this] with the given fields changed
  _NoneOrExampleClass copyWithAlt({
    // For consistency's sake, you could make every field NoneOr<T> even if
    //  the field is non-nullable. That way it would read the same from a 
    //  logical perspective.
    NoneOr<bool> nonNullableField = const None(),
    NoneOr<bool>? nullableField = const None(),
    // ...
  }) =>
      _NoneOrExampleClass(
        nonNullableField: None.fallback(nonNullableField, this.nonNullableField),
        nullableField: None.fallback(nullableField, this.nullableField),
        // ...
      );

  _NoneOrExampleClass copyWithAlt2({
    NoneOr<bool> nonNullableField = const None(),
    // Note that NoneOr<bool?> is effectively the same as NoneOr<bool>?
    //  or even NoneOr<bool?>? - use whatever syntax most appeals to you.
    // In the future, we may introduce a custom lint rule to dictate where
    //  we can put the "?" character
    NoneOr<bool?>? nullableField = const None(),
    // ...
  }) =>
      _NoneOrExampleClass(
        nonNullableField: None.fallback(nonNullableField, this.nonNullableField),
        nullableField: None.fallback(nullableField, this.nullableField),
        // ...
      );
}

// ... 

void _exampleUsage() {
	// baseline
	_NoneOrExampleClass e0 = _NoneOrExampleClass(
		nonNullableField: true,
		nullableField: true,
	); // Example(true, true)

	// copies using copyWith:

	// simple example with non-nullable underlying fields:

	// 1. If field is ommitted, null is assumed, and existing value is kept (true)
	_NoneOrExampleClass s1 = e0.copyWith(); // Example(true, true)
	// 2. If field is marked null, existing value is kept (true)
	_NoneOrExampleClass s2 = e0.copyWith(nonNullableField: null); // Example(true, true)
	// 3. If field is given a value, value given overwrites existing (false)
	_NoneOrExampleClass s3 = e0.copyWith(nonNullableField: false); // Example(false, true)

	// simple example with nullable underlying fields:
	
	// 1. If field is ommitted, None() is assumed, and existing value is kept (true)
	_NoneOrExampleClass s1 = e0.copyWith(); // Example(true, true)
	// 2. If field is marked null, existing value is overwritten with null (null)
	_NoneOrExampleClass s2 = e0.copyWith(nonNullableField: null); // Example(true, null)
	// 3. If field is given a value, value given overwrites existing (false)
	_NoneOrExampleClass s3 = e0.copyWith(nonNullableField: false); // Example(true, false)
}

Performance

Early testing suggests that this approach is less performant than alternate approaches such as using a second boolean variable to set a field. Run on your own system for comparison, but the rudimentary tests suggest that due to the overhead of calling additional functions and comparing types, the approach of using the second variable performs around ~100% faster than an approach of manually testing for type in a ternary operator.

The methodology used to test was to run three versions of copyWith over 10,000,000 runs. In fairness, that is an unlikely impediment for casual use copying a few values at a time, but if your usecase requires millions of copies over a long time horizon, you may wish to consider the runtime implications of this package's approach: this may save dev time and provide convenience to developers at the expense of runtime optimizations.

See none_or_test.dart for the code


Additional information

Right now there is no linter rule to differentiate the different ways of using NoneOr<T> in your code. In the future, that may change. PRs are always welcome.


Deprecation

It is a sincere hope that at some future point this package will be rendered obselete by future updates to the dart language. In the meantime, this is meant as a stopgap to provide the necessary functionality into the dart language in a way that aims to benefit from the type safety dart provides, while enabling unintended behavior.

It is also possible that future updates to the dart language will further block the workarounds this package uses in an attempt to crack down on this workaround. Use at your own risk.

Libraries

none_or
Provides a new typedef NoneOr<T> and a new class None<T> to enable differentiation between explicit values given to a field that may be null versus providing no value to a field. Expected use case is for copyWith functions.