Language: Union types

Created on 5 Sep 2012  路  54Comments  路  Source: dart-lang/language

Currently Dart does not support function overloads based on parameter type and this makes it verbose/awkward to clone/wrap Javascript APIs which accept multiple types.

For example, WebSocket.send() can accept a String, a Blob or an ArrayBuffer.

In Dart, this can either be exposed as:
WebSocket.sendString, sendBlob, sendArrayBuffer which is verbose
or:
WebSocket.send(Dynamic data), which is no longer a self-documenting API.

It would be great to have some variant of union types, along the lines of JSDoc:
WebSocket.send(String|Blob|ArrayBuffer data);

state-duplicate

Most helpful comment

Another case that might be helped by Union Types.

Today:

new Padding(
  padding: new EdgeInsets.all(8.0),
  child: const Card(child: const Text('Hello World!')),
)

Could be:

new Padding(
  padding: 8.0,
  child: const Card(child: 'Hello World!'),
)

All 54 comments

_Set owner to @gbracha._
_Added this to the Later milestone._
_Added Accepted label._

_This comment was originally written by jjo...@google.com_


Another use-case:

In the thread "Too type happy in dart:io?", mythz complained about the verbosity of setting the Content-Type of an HTTP response:

response.headers.contentType
      = new ContentType("application", "json", charset: "utf-8");

Instead, it would be nice to say:

response.headers.contentType = "application/json; charset=utf-8"

However, Anders noted that it's nice to have Content-Type reified as an object, so that you can access properties like so:

response.headers.contentType.mimeType

In the context described above, type-unions would allow the contentType setter to accept either a ContentType object, or a String; in the latter case, the String would be immediately parsed into a ContentType.

Any chance of this getting another look from the standards committee? I've run into several cases recently where I really want union types.

_Removed this from the Later milestone._
_Added Oldschool-Milestone-Later label._

_Removed Oldschool-Milestone-Later label._

_This comment was originally written by @stevenroose_


Another important use case is auto-complete in IDE's.

If a library declares a method with a dynamic return type, IDE's won't show any autocomplete options. If the method can only return two types of objects, the types could be specified using the union operator and IDE's could show auto-complete values for both types.

Example:

class MyClass {
  ...
  
  Map|String toJSON([bool stringify = false]) {
    Map jsonMap = _generateMap();
    return stringify ? const JsonEncoder.convert(jsonMap) : jsonMap;
  }
}

Isn't this relevant for the non-nullable proposal? Ceylon does this really nice thing where null es of type Null, so a nullable String is declared as String? which I believe is short hand for the union String | Null.

I find it somewhat strange that this is not higher on the priority list of the dart team. This feature really seems like an essential to me, that should have been in dart 1.0.

@cgarciae was thinking the same thing myself, union types seem like more bang for the buck. Maybe file a bug against the nullable types DEP?

According to discussion in https://github.com/dart-lang/sdk/issues/20906 union types were (are?) implemented in the old Dart Editor behind a flag. As far as I understand it, they were being inferred by the editor and used for hints and (possibly) code completion. There was not a syntax to declare a value as having an union type, though, which is useful for documentation purposes and it is what this bug is about.

I think a great example of a union type use case is JSON:

JSON = Null | bool | num | String | List<JSON> | Map<String, JSON>

Ah, a _recursive_ union type. No need to make things simple, eh? :)

I think that might just be too "wide" a union to actually be useful. To actually use the type, you have to check against almost every type anyway:

if (x == null) { 
  ...
} else if (x is bool) { 
  ... 
} else if (x is num)  {
...
} else if (x is String) {
...
} else if (x is List) {  // This should be enough to deduce that x is List<JSON>?
...
} else {  // Here you don't have to write (x is Map), that can be inferred.
...
}

That does expose an advantage to having the union type. I think we should be smart enough to know when an imprecise type test still only allows one type of the union, so:

JSON x = ..;
 if (x is List) {  .. x is known to be List<JSON> here ... }

We can't do that with the current dynamic.

For smaller unions like A = B | C, a negative B test is as good as a positive C test. Classical example:

var result = f(value);  // Inferred type (S|Future<S>).
if (result is Future) {  
  // It's Future<S>
} else {
  // It's S.
}

Maybe we can even be cleverer:

var x = something();   // type (List<int>|Set<int>|Map<int,int>).
if (x is Iterable) {
   // x has type (List<int>|Set<int>|(Map<int,int>&Iterable<int>)), not just Iterable<int>.
   if (x.contains(42)) print("Huzzah!");  // valid, all three types expose Iterable.contains.
} else {
   // x has type Map<int,int> - the List and Set parts could not refute the is test.
   if (x.containsKey(42)) print("Yowzer!");
}

If we have union types, I want this program to be warning free! :)

It might not be as simple as it looks, though :(

I'm really doing Boolean algebra on types. A type test creates an intersection of types:

T1 x = ...l
if (x is T2) { 
  // we know x is (T1&T2).
}

If T2 is a subtype of T1 then the result is just T2. That's what we currently support.
If T1 is a subtype of T2, the test is a tautolgy and you don't get any smarter. the result is still T1.
If T1 and T2 are unrelated, then you just get an intersection type. That may be an empty type, and you can't generally use it for anything.
Now, if T1 is a union type S1|S2, doing intersection should give us (S1|S2)&T2 which should be equivalent to (S1&T1)|(S2&T1).

The problem is that a positive is test can't refute any type because it's generally possible to implement any two interfaces on the same object. The negative test can refute something - if you are a Map, List or Set, then knowing that you are not an Iterable definitely precludes being a List or Set.

The possibilities are endless :)

If we had union types, Flutter APIs that deal with text get much nicer.

Instead of this: title: new Text('Example title') I could do: title: 'Example title' because we could annotate title named parameter has taking either String or Text.

Another case that might be helped by Union Types.

Today:

new Padding(
  padding: new EdgeInsets.all(8.0),
  child: const Card(child: const Text('Hello World!')),
)

Could be:

new Padding(
  padding: 8.0,
  child: const Card(child: 'Hello World!'),
)

So is this still being considered? Dart is a great language, but this is one huge thing that it's missing.

Watch https://youtu.be/9FA3brRCz2Q?t=13m41s and pay attention to FutureOr and whereType. These problems are just begging for union types. Additionally union types are a good way of dealing with null safety.

I've come from 3 years of TypeScript development over to Dart via Flutter. Many things I like about Dart, some things I don't like so much but can deal with. The lack of union types (and non-nullable types) are really loathsome. For this reason I much prefer using TypeScript. My code is shorter, more type-safe and more self-documenting. If these features make it to Dart I'd feel so much more comfortable with Flutter.

pay attention to FutureOr and whereType. These problems are just begging for union types

Well, FutureOr has been described many times as a way to check out union types "in a sandbox". However, union types and intersection types in their general forms probably cannot be separated, and there's a huge difference between recursive union types and non-recursive ones, so it's definitely a non-trivial step to leave the sandbox (and it's safe to say that this step is not guaranteed to be taken).

I'm interested in support for algebraic data types, similar to Swift's "enum with associated values" or kotlin's sealed class. Is this the right issue for that type of support? Or should I file a separate issue?

An algebraic data type (say, SML style) could be characterized as a tagged union, but union types as discussed here are untagged. So with the algebraic datatype Data = Int of int | Char of char; you'd need to unwrap a given Data to get to the int resp. char that it contains, but with a union type you'd just have an int or a char at hand already, with no wrapping. This matters in a lot of ways, so I'd consider algebraic data types to be a different topic.

However, since OO objects carry their own (type) tags already, you could claim that we don't want the wrapping, we just want to establish a guarantee that a given type T is sealed (i.e., that T has a finite and statically known set of subtypes, e.g., because they must all be declared in "this file" or something like that). You could then write code that recognizes each of the subtypes of T and does different things with them, similar to pattern matching code with an SML style datatype. I'd think that this would be yet another topic: 'Sealed classes'. ;-)

(The reason why I don't think it's just the same topic as algebraic data types in disguise is that sealed classes can be used for other purposes as well, e.g., making sure that we can reason about all implementations of a given method, because we know every one of them statically.)

Honestly I'd be happy with just the when keyword from Kotlin to simplify if elseif elseif else

That should certainly be a separate issue (and I don't think we have an existing issue which covers that request).

Union types + classes + typedef

Edit: This post has been updated quite a few times to reflect the current discussion status. Also see:

Instead of introducing a special syntax just for ADTs I'd like to suggest a solution inspired by TypeScript, Rust and polymorphic variants (example follows below):

  • | for constructing union types (i.e. an unordered set of types)

    • no special sealed or enum keyword

    • you can combine types (including other unions) from different modules

    • minimal syntax and works ad-hoc in function definitions (great for simple cases: no need to name the union or have a separate definition)

    • if a function expects type A | B | C it's valid to pass a subset type like A | C

    • you can define functions that expect just one variant/case (or any subset) instead of the whole union/enum (catches mistakes at compile-time that ADTs can only catch at runtime)

    • can later be used to express nullability in a generic way

    • you can extend an existing function taking A to A | B without breaking the API

    • a lot more flexible and extensible than ADTs while still allowing exhaustive pattern matching for safety

  • typedef for (optionally) giving a union a name

    • simply reuses the existing Dart syntax, nothing new to learn

  • syntax sugar for defining classes inline (based on dart-lang/language#1002 and dart-lang/language#314)

    • reuses a syntax that could become a standard for class definitions and thus be generally useful

    • reuses the normal class concept (less concepts in the compiler, any optimizations necessary for variants would automatically benefit normal classes, too)

    • allows defining methods or even using extends for variants

    • for complex variant definitions you can move the variant out of the typedef

So, typedef + | + class syntax sugar replaces Kotlin's sealed (or Swift's enum) and is more flexible (every individual concept this is based on is useful on its own):

// Let's define two variants (normal classes) outside of a typedef:

// Syntax sugar from dart-lang/language#1002
class Success<T>(String authToken, T data);

class UnexpectedException(Exception _exception) {
  Exception get exception => _exception;
}

// Here we use the same syntax sugar (without "class" keyword)
// as above to define variants inline.
// Every variant that has parentheses is a class defined inline,
// while without parentheses you refer to existing classes.
typedef LoginResponse<T> =
  | Success<T>  // refers to existing class
  | UnexpectedException  // refers to existing class
  | InvalidCredentials(int code) extends SomeOtherClass()  // defines new class
  | AccountBlocked(String reason);  // defines new class

typedef LoginError = InvalidCredentials | AccountBlocked;

String handleLoginError(LoginError | UnexpectedException error) {
  // Simplified switch syntax for exhaustive match
  switch (error) {
    // Note how we can unpack via the constructor's field definition list
    AccountBlocked(reason) {
      return reason;
    }
    UnexpectedException(exception) {
      return exception.toString();
    }
    // With "_" we tell the compiler we're not interested in the "code" attribute
    InvalidCredentials(_) {
      // ... update UI ...
    }
  }
  // Also a nicer Rust-inspired if-let variant that does pattern matching
  if (error is AccountBlocked(reason)) {
    // Note that we can directly use the unpacked reason variable
    return reason;
  }
  return "default...";
}

Pattern matching

We should literally match on all variants, no matter if they contain parent and child classes:

class Parent();
class Child() extends Parent();

typedef X = Parent | Child | int;

X x = ...;
switch (x) {
  Parent() -> ...
  Child() -> ...  // Would fail at compile-time if this were missing
  int -> ...
}

TODO:

Should we extend switch (like in the examples above) or introduce a separate match expression?

Maybe? If you want to match on the class hierarchy instead:

switch (x) {
  is Parent -> ...
  int -> ...
}

We could also allow any other binary operator (not just is):

switch (someInt) {
  > 10 { ... }
  > 0 { ... }
  default { ... }
}

We could allow switch/ match to be used as an expression:

String result = switch (x) {
  Parent() -> 'parent'
  Child() -> 'child'
  int -> 'int'
}

But how do we define the result when using {} blocks instead of ->? Should the last statement be treated specially?

switch/ match could also allow matching on the next element from one or more async streams like select on Go's channels (implementing a type-safe async any/or for multiple Futures).

Subtyping rules

A union type would behave like the common interfaces implemented by all of its variants. So, int | double implements num and you can call .round() without matching on the type. You can also pass it to any function taking a num argument.

The subtyping rules are based on Scala 3:

  • A is always a subtype of A | B for all A, B (and A | A gets reduced to just A).
  • If A <: C and B <: C then A | B <: C
  • | is commutative and associative:

    • A | B =:= B | A

    • A | (B | C) =:= (A | B) | C

So, | basically creates a flat unordered set such that (int | int) | int is just int and int | double is the same as double | int. Note that this is slightly different from TypeScript where a union type is an ordered set.

TODO: Intersection types (if also supported)

If we also add intersection types (commonly expressed with &) the subtyping rules get extended with:

  • & is distributive over |:

    • A & (B | C) =:= A & B | A & C

TODO: Extensions (once supported by Dart)

Extensions would allow adding interfaces to existing variants of union types and maybe they could even add methods to the whole union type (i.e. add an implementation to all variants at once).

TODO/Maybe: Interface requirements

We might also want to have a feature for "require interface X for type T" where T can be a union type:

mustImplement LoginResponse<T> with Foo, Bar;

Then you'd be statically forced to implement the Foo and Bar interfaces (e.g. directly via inheritance/mixin or via extensions) on each of the types that belong to the union. This feature is not strictly necessary because a missing interface would still raise a compiler error when e.g. trying to access a given method, but it wouldn't raise an error if nobody ever uses a given interface. When writing a reusable package you might want to statically guarantee that the interface you're exposing actually adheres to the spec.

Type narrowing

Similar to TypeScript, match and if-statements that check the type automatically narrow a variable's type in the matching block.

Generic types

It should be possible to take the union of two generic types (S | T).

TODO:

What if you switch/match on the type of S | T? With T = S you'd have two different code paths matching on the the same type. Should the first matching case win or should this result in a compile-time error?

How can we apply narrowing to generic types?

TODO/Maybe: Overloading

With overloading you could define functions like this:

void doSomething(int x) {}
void doSomething(double x) {}

int | double x = ...;
doSomething(x);

If the union type contains both a parent and child class and the overloaded function takes multiple arguments this might require multiple dispatch to work properly. Initially Dart could disallow function overloading with ambiguous union types.

TODO/Maybe: Literal types

Something else I really like about TypeScript is that you can use literals as types (especially strings). This way you can e.g. make APIs safe that only accept certain constants:

const KeyA = 'a';
const KeyB = 'b';

// You can even refer to a const here
typedef Key = KeyA | KeyB | 'c' | null;

void setValue(Key key, String value) {
  // ...
}

With some codegen literal unions could even be used for e.g. making Flutter's rootBundle.loadString(path) statically allow only valid paths that actually exist (the typedef of allowed paths would be generated at compile time).

Changelog

  • Using switch instead of match in the examples, but it's still an open question which one should be used
  • Added subtyping rules from Scala 3
  • Explained how matching on Parent | Child would work
  • Using normal classes instead of introducing a new data classes concept. Only syntax sugar is needed.
  • Added short notes about extension methods, overloading, and narrowing.

For the improved static checking that union types would give us, it might be useful to take a smaller step in the shape of a lint. See https://github.com/dart-lang/linter/issues/1036 for a request in this direction.

_tl;dr Read the bold text rd;lt_

@wkornewald, thanks for a nice description of a notion of union types!

I do think it's worth pointing out a specific distinction:

| for constructing unions [plus properties: can combine types .. from different
modules, A|C <: A|B|C, nullable T is simply T|Null)

These are properties that I'd expect from any proposal about union types, and you should be safe assuming that you'd get them.

However, sealed is a different mechanism with a different set of properties and purposes: It's associated with a specific subgraph _S_ of the type hierarchy, presumably declared in a single library _L_, and it prevents _S_ from being extended by declaring new subtypes of anything in _S_ anywhere outside _L_. So if you want to use sealed to emulate union types it's going to be a _really_ weak and restricted emulation.

In contrast, union types won't ever allow the compiler to know that any constraints apply to a specific subgraph of the subtype relation, so union types will never serve to improve the opportunities for inlining and other optimizations that may only be valid if you can establish a (modular!) compile-time guarantee that every possible implementation of a given method is known. Similarly, if a human being or proof system is reasoning about the behavior of a certain interface it may be very helpful to know every possible implementation of the methods under scrutiny. Union types are not just weak when it comes to establishing completeness guarantees ("we've seen them all") which are known at the declaration of a type, they're irrelevant.

You mentioned one more property, and if I understood it correctly then it's actually just one more example of a difference between a sealed set of classes and a union type:

you can define functions that expect just one variant/case instead of the whole union/enum

If you have the sealed set A, B, C with the relations B <: A and C <: A then we can emulate the unions A|B|C, B, and C, but not A, A|B, A|C, nor B|C. So you don't have to restrict yourself to just the whole sealed set (A|B|C), but you also won't get all subsets. And this just reconfirms that sealedness is not unions.

So we shouldn't consider union types and sealedness as alternatives, they are simply different.

That said, I think your examples provide a nice illustration of how several independent mechanisms can interact constructively (value classes, class declaration sugar, pattern matching). That's an important motivation when each one of them is considered.

Lets take an example, File | Directory.
Often times I dont really know what's so common about the two types I want to be possible as a param, I may not want or view them as common at all. I just happen to want X | Y here. Why should I be required to make a Z? Figuring out what Z could be called is often not trivial, just look at the above.
And even if you do come up with a name that fits both, suddenly the code doesnt look so intuitive and nice at all anymore. (did you manage to come up with inode?)
I mean think about that, I never even had a Z in mind at all. That's a future optimization. In my mind this is a boolean: X or Y. Suddenly the language requires me to step completely out of my thoughts and reason about yet another thing, namely Z that I don't even need yet or maybe never.

Simply adding the union keyword to the language would make this clear (and less verbose than all the | ones)

// this can be scoped inside a class, kept private by amending the _ if needed
union websocketInput { String, Blob, ArrayBuffer }

@mathieudevos I would prefer using the | syntax since it's not only already used in TypeScript and F# (which would make for a smoother learning curve), but it also improves readability since it clearly differentiates it from the Map en Set literal syntax ({ }) and can be easily read as a sentence.

I read typedef MyUnion = String | int as "the type MyUnion is equal to a String OR an int".

@mathieudevos Adding to what @jeroen-meijer (and @Meai1) said: The | syntax allows for simple ad-hoc unions:

bool isAllowed(String | int x) { ...}

With explicit union this becomes unnecessarily verbose and requires defining an extra type:

union AllowedTypes { String, int }
bool isAllowed(AllowedTypes x) {...}

This gets worse quickly if you have many different small unions in your code which all must be given a meaningful name (you surely know how difficult that can sometimes be and how it breaks the train of thought). Also, this would have to be taken into account for autocomplete because a name like AllowedTypes is not helpful when you have to jump to definition for each union to see what is allowed. Even if autocomplete shows the underlying types of the union (which it should, even for the typedef + | solution) you still get to read the name of the union in cases where it doesn't add any value, so the code just appears more complicated than it has to be.

@eernstg I still owe you a reply after this long time. :( Unfortunately, the new discussion came up on a different issue, so I'll just link to it and maybe we can continue discussing there (or here if you prefer): https://github.com/dart-lang/language/issues/83#issuecomment-531315843

Hopefully this becomes a thing, so this wouldn't just be used as documentation on the intended usage.

Is this language still being developed?

Still waiting for optionals and union types for years now. Like major language features.

As of now, we can't parse our banking API since they return false|object on many items.

馃檲

Hopefully this becomes a thing, so this wouldn't just be used as documentation on the intended usage.

The exact problem i found today, this must be part of the language

@creativecreatorormaybenot no thanks. Might as well write JS or Python and introduce bugs.

@creativecreatorormaybenot that shouldn't be a solution lol - being able to type check at compile time is much more welcomed.

@creativecreatorormaybenot Nothing is that necessary if your attitude is that everything can be "fixed" with a mountain of boilerplate. May as well go back to assembler.

just a suggestion.
I try to use Object instead of dynamic whenever I can.
Using dynamic you can call a non existent method and have a runtime exception.
Using Object you must cast it to something before calling a method or property.
And with extension methods you can easily and securely cast stuff.
It works even on null as in the example below:

extension UtilExtensions on Object {
  Map<String, Object> get asPair {
    assert(this is! Future);

    if (this != null && this is Map) {
      if (this is Map<String, Object>) {
        return this as Map<String, Object>;
      }

      return (this as Map).cast<String, Object>();
    }

    return null;
  }
}

void main() async { 
  Object a;

  print(a.asPair); // prints null and no exception is thrown

  var b = {};

  print(b.runtimeType); // JsLinkedHashMap<dynamic, dynamic>
  print(b.asPair); // {}
  print(b.asPair.runtimeType); // CastMap<dynamic, dynamic, String, Object>
}

I wanted to point out that union types can be worked around currently in Dart and not that union types are not useful.

This means that Dart currently supports a (bad) way to work with union types and that this means that you can build your product in Dart today.

@creativecreatorormaybenot this isn't a thread about workarounds. This shouldn't be a thing and that's all there is to it: https://github.com/dart-lang/sdk/issues/4938#issuecomment-573634221 - you shouldn't need to look at the sdk source to understand the intended use of a parameter.

You don't need to look into the sources, it's in the documentation:

A value in the map must be either a string, or an [Iterable] of strings, where the latter corresponds to multiple values for the same key.

Union types can be super expressive (I know them mainly from Python), but it's something that mainstream programming languages lived without for decades, so I understand there isn't much urgency in implementing this. (Why don't we yet have independently developed linter that would interpret comments such as the one in the sdk and typecheck on it? As I say, lack of urgency for this feature, on all involved parties in this discussion.)

Discussions of workarounds certainly belong to issue threads, I have no complaints about that. It's not exactly the sort of updates I am looking forward to here, though :P

but it's something that mainstream programming languages lived without for decades, so I understand there isn't much urgency in implementing this.

With a minute of Google I found an implementation in Algol from 1968 :laughing: Hm, radical enhancements have been made even in the last decade, any language that ignores such things will be left behind. Dart and Go are so conservative, I get that a lot of people like this. To me it's kinda boring. I'd like to see a language where you can plug in syntax and semantic extensions through configuration. That would probably give Google nightmares 馃槀

@jiridanek I completely agree, actually, a linter is all we really need for things like optionals (or non-nullable as the dart team calls it)

Regarding the reason for Union types.

Sometimes they can also work like optionals ("?") with Model|null (Although optionals is coming https://github.com/dart-lang/language/projects/1)

When working with APIs sometimes objects can return different data types. For example, the last banking app we made had an API with properties that returned false|Model2|Model1 on a single property; not our choice.

Really, most errors we are seeing across our 36 commercial Flutter Apps come from not being able to define optionals and union types on model definitions.

In the professional world being able to define models accurately is a must. It's one of the main ways to avoid unnecessary bugs.

That false|Model1|Model2 return value would be considered a "bad API design" in languages with static typing but without union type support. Even with that is available, the result can be hard to follow. There isn't a natural name for the union (this was discussed in prior comments), so when you think of the subsequent program execution, you have to always think about each of these eventualities separately. The types are not helping you reason about the program that much. Union types don't look to be on the horizont and I won't write my own linter for this, so I'd personally avoid writing such methods, to benefit from the type system I do have already.

The situation with optionals and union types for method arguments is different in my mind. Optionals as return types do work well in practice, and union types for arguments are essentially method overloading in Java, except without having to write extra method for each combination of types. I do miss union types a lot here.

This would make a lot of sense to increase the soundness of Dart. I still find myself resorting to dynamic too much.

Yeah Union types and iterable object would make the dart flauless.

@jiridanek yes I agree it's bad API design but in the real world sometimes you don't have a choice.

but it's something that mainstream programming languages lived without for decades, so I understand there isn't much urgency in implementing this.

With a minute of Google I found an implementation in Algol from 1968 馃槅 Hm, radical enhancements have been made even in the last decade, any language that ignores such things will be left behind. Dart and Go are so conservative, I get that a lot of people like this. To me it's kinda boring. I'd like to see a language where you can plug in syntax and semantic extensions through configuration. That would probably give Google nightmares 馃槀

Rarely are languages "left behind" on their own merit. A language is nothing without use-cases. If developing the Dart SDK and Flutter do not need these features, then Dart will not have these features.

Based on your preference as stated, I'd suggest Javascript.

As someone new to Dart and Flutter coming from Typescript and React Native this confused me right off the bat! I just wanted to type a Map<String, String | bool> but found unions missing from the language.

It seems really odd to me that this basic feature is missing from the language. There are (IMO) plenty of extremely common use cases for it. Most commonly as was pointed out previously, JSON as typedef JSON = Null | bool | num | String | List<JSON> | Map<String, JSON>.

All sorts of domain specific types will require unions, especially when you have to model an externally imposed type that you don't have control over.

So what are the workarounds? Just use dynamic and manually check for correct types at runtime? That isn't a very good one as it loses type safety at design time for anything that interacts with this interface... Are there any good workarounds? Is there a consensus on and strong reason for why this seemingly common feature is not part of the language?

@AndrewMorsillo was opened 2012, September; don't hold your breath on seeing this :-|

So what are the workarounds? Just use dynamic and manually check for correct types at runtime? (...) Are there any good workarounds? (...)

The common approach in the Dart/Flutter community in 2020 (that I personally see often) is to either use some code-generation libraries to generate union types e.g. freezed or use library like sealed_unions or union.

Example union class in freezed:

@freezed
abstract class Union with _$Union {
  const factory Union(int value) = Data;
  const factory Union.loading() = Loading;
  const factory Union.error([String message]) = ErrorDetails;
}

This of course isn't so nice as native support for unions, but gives you a lot of freedom in domain modeling and provides type safety.

What @orestesgaolin says.

Object oriented languages rarely need union types because an interface is a union type.
When you want to do a union over unrelated types, you can just make a wrapper hierarchy, and expose whatever common behavior your objects have directly as an interface (even if it's only detecting which type it is).
If we had traits or interface injection, you could make that even easier.

What's holding people back is not lack of power, but lack of convenience. We don't want to have to write the wrapper - not the declaration (which is mostly boilerplate), and not the wrapping at the use-site, which is inconvenient to work with and easy to forget for users of an API because it's only there for technical reasons, not conceptual. We want untagged unions because tags can be inconvenient.

The typical example is JSON. The mistake here was to parse JSON into plain objects. If we had parsed JSON into a JsonObject which was either a JsonValue<T> (T being int, double, bool, String or Null) or a JsonList or JsonMap, where each object had some convenience signature, and where the values exposed by JsonList and JsonMap were again JsonObjects, then you wouldn't need the union type.
Using the untyped parsed JSON structure directly is the real problem here, not that the language can't type it.

There are obviously other examples too, mainly around interop with JavaScript, where an underlying union-type leaks through.
Or where we have designed an API during the Dart 1 days to, say, take either an InternetAddress or a String, for convenience.
Again, a wrapper class could work, and again it's too inconvenient in practice, or too inconvenient a breaking change to be a priority to change. If the types of the union are otherwise unrelated, the wrapper won't have any useful functionality anyway.

So, solutions to needing multiple disjoint types in the same place could be:

  • Union types
  • Interface injection/traits.
  • Convenient union-wrappers with automatic coercion.

Pure union types is probably the most complex solution, with the most far-reaching effect on the type system.

Deferring this discussion to #83.

Was this page helpful?
0 / 5 - 0 ratings