Union2<A, B> typedef

void Union2 (
  1. void (
    1. A value
    ),
  2. void (
    1. B value
    ),
  3. Object _c,
  4. Object _d,
  5. Object _e,
  6. Object _f,
  7. Object _g,
  8. Object _h,
  9. Object _i
)

A function that holds a value which can be of different types, and allow manipulating that value in a type-safe way.

Unions comes in different variants, where its name "Union" is suffixed by a number (such as Union2 or Union3). That number represents the number of different types the value stored can take.

For example, if a value can be both a String and a int, that makes two different types. We can therefore use Union2 like so:

Union2<String, int> myUnion;

or:

Union2<int, String> myUnion;

Note that we cannot assign Union2<int, String> to Union2<String, int> (or the opposite).

On the other hand, it is possible to assign Union2<int, String> to Union3<int, String, Whatever> (but not the opposite).

Creating a union

Due to a language limitation, we cannot do:

Union2<String, int> foo = 42;
foo = "hello world";

Instead we have to wrap our value into another object. To do so, union expose a bunch of extension methods to transform our value in a union, which goes from asFirst to asNinth.

This number represent the index that our value correspond to in the type list that the union can take.

For example, 42.asFirst allows creating:

Union1<int> union1 = 42.asFirst();
Union2<int, SomeType> union2 = 42.asFirst();
Union3<int, SomeType, AnotherType> union3 = 42.asFirst();
...

Whereas 42.asSecond allows creating:

Union2<SomeType, int> union2 = 42.asSecond();
Union3<SomeType, int, AnotherType> union3 = 42.asSecond();
Union4<SomeType, int, AnotherType, YetAnotherType> union4 = 42.asSecond();
...

As such, our original example becomes:

Union2<String, int> foo = 42.asSecond();
foo = "hello world".asFirst();

Converting a union to another union

Any union can be freely upcasted to unions that takes more types.

For example, we can do:

Union2<String, int> union2;

Union3<String, int, double> union3 = union2; // upcast, no problem
Union4<String, int, double, Object> union4 = union2; // still works
...

But we cannot downcast unions. It is therefore impossible to do:

/// ```dart Union3<String, int, double> union3;

// downcast, does not work because the value can be a double too. Union2<String, int> union2 = union3;


# Reading the value

Unions expose their current value publicly through the getter `value`.

But since the value can be of different types, a small trick is necessary
to preserve type safety:

On a `Union2<SomeType, AnotherType>`, the type of `value` is neither of
type `SomeType` nor of type `AnotherType`.
Instead it has the type of their nearest common interface.


Union2<String, int> example; // String and int have no shared, so value is of type Object Object value = example.value; int impossible = example.value; // COMPILE ERROR, not type safe

Union2<int, double> example2; // int and double have a common interface: num. Therefore is of type num num value2 = example2.value; // type safe, there's no cast or compile error. int stillImpossible = example2.value; // COMPILE ERROR, value is a num not a int

Union2<String, String> example3; // The type is always a String, so value is a String too String value3 = example3.value; // type safe, there's no cast or compile error.


# Applying operations on the value

To prevent mistakes, it is not recommended to use the `is` operator to
determine what the value currently is.

**DON'T:**

Union2<String, int> union; if (union.value is String) { print('String: ${union.value}'); } else if (union.value is int) { print('int: ${union.value}'); }


This is a bad practice because it is easy to forget to handle one of the
potential types that the value can take.

Instead, use one of the availble methods on unions, such as:
- `map`, which transforms the current value in another value.
- `switchCase`, which allows performing some logic based on the value type.
- `join`, which fuse all the different types in a single type.

**DO:**

Union2<String, int> union; union.switchCase( (String value) => print('String: $value'), (int value) => print('int: $value'), );


This is better because the code will not compile if we forgot to handle
one of the potential types that `value` can take.

# Limitations

Because they are not part of the language, but implemented on the top of it,
this implementation comes with a few limitations.

- the operator== of unions cannot be overriden. This means that two unions
  containing the same value may not be equal:

final a = 42.asFirst(); final b = 42.asFirst(); print(a == b); // false


  Instead, compare their `value`:

final a = 42.asFirst(); final b = 42.asFirst(); print(a.value == b.value); // false

- `Union2<String, int>` cannot be assigned to `Union2<int, String>`.
  Instead you can use `join` to convert one to another:

Union2<String, int> union = "hello world".asFirst();

Union2<int, String> converted = union.join( (v) => v.asSecond(), (v) => v.asFirst(), );


# Custom unions

Sometimes we want to reuse one specific combination of type often.
In that situation, we'll want to write a type alias.

Ideally, we'd want:

typedef Result = Union2<T, Exception>;


Sadly, Dart does not support typedefs of typedefs (but it may come soon).
See https://github.com/dart-lang/language/issues/65

Instead as a temporary workaround, we can _expand_ the typedef.
As such, the actual implementation of such `Result<T>` would be:

typedef Result = void Function( void Function(T value), void Function(Exception value), Object _c, Object _d, Object _e, Object _f, Object _g, Object _h, Object _i, );


This is of course not ideal, but there's an easy way to implement it:
- go to the definition of the union you want to reuse (here `Union2`)
- copy its implementation
- paste it and update it to match your needs.


On the hand, while the initial boilerplate is a bit boring, this
implementation has some benefits.
Our custom `Result<T>` is assignable freely to `Union2<T, Exception>`:

Union2<int, Exception> union2; Result result = union2; // works without issue union2 = result; // works too

Implementation

typedef Union2<A, B> = void Function(
  void Function(A value),
  void Function(B value),
  Object _c,
  Object _d,
  Object _e,
  Object _f,
  Object _g,
  Object _h,
  Object _i,
);