Dart 2: Legacy of the `void`

A semi-accurate depiction of the universe of void-like types in Dart2

One of the questions I see the most asked on StackOverflow, Gitter, and even Google-internal support channels is the difference between the following built-in types in Dart 2: Object, dynamic, void, and Null.

Long-story short, Null (or Bottom in other languages, i.e. “Nothing”) shouldn’t be used in most real user-code, and I suspect we’ll see more articles and lints in the near future to gently discourage usage.

The rest of the three are not as clear, because something in Dart 2 anything can be dynamic, Objector voidat runtime, varying only by the static type signature. So let’s look at a few practical examples of when you should use which type signature.


Object

Objectis the root class of the Dart class hierarchy, and every other class in Dart is a sub-class of Object — including “primitive” types like int, double, or bool. It guarantees a few things: a hashCodeproperty, an ==operator, a toStringmethod.

Practically speaking, I use Objectlike a poor man’s union type — expecting users to use the isoperator to determine the real type of something before using it. I don’t use dynamic, because, as outlined in the next section, it disables important static analysis and more easily allows you to get into an invalid state.

Object readProperty(String name) { ... }
void main() {
var age = readProperty('name');
if (age is int) {
print('I am $age years old');
} else if (age is String) {
print(age);
}
}

Another option is to use Object to declare you don’t care what the inner type of a data structure is, for example List<Object> might mean “a list of anything”. This comes in handy when, for example, writing a function that combines the hashCode of every element in a List:

int hashList(List<Object> elements) { ... }

A nice property of Object(compared with dynamic) is that you will get immediate analysis and compiler feedback if you try to call a method on it that doesn’t reliably exist. For example, the following produces a static error:

void main() {
Object a = 5;
a.aMethodThatDoesNotExist();
}

In practice though, Object is fairly(and intentionally) limited. My hope is that Dart will get support for method overloads and that will allow me to dramatically decrease my usage of the Object type in real code.

dynamic

I personally never use the dynamic type in Dart 2. From my perspective, it is sort of a union of Objectand a special instruction that tells tools and compilers to disable static analysis checks. That is, the following is legal, and will only present an error at runtime (not statically!):

void main() {
dynamic x = 5;
x.aMethodThatDoesNotExist();
}

In Dart 1, dynamic was everywhere, and any other static type was for IDE and static analysis support — but the compiler (and runtime) treated everything as dynamic. There are still some unfortunate “gotchas” in Dart 2 that can accidentally create a dynamic-typed variable, though:

computeAge() => 5; // Return type is dynamic
void main() {
var name; // Static type is dynamic
var animals = []; // Static and runtime type is List<dynamic>
}

Worse yet, dynamic calls lose type information that is vital in Dart 2:

class User {
String name;
}
void main() {
var users = []; // Implicitly List<dynamic>, remember?
users.add(new User()..name = 'Matan');
  // Runtime error: List<dynamic> is not a Iterable<String>
Iterable<String> names = users.map((u) => u.name);
}

The reason for this error is because the actual call here is:

users.map((dynamic u) => u.name);

… which does not have enough static type information to produce a Iterable<String>. By fixing users to have the proper type (and avoiding dynamic calls), everything works:

void main() {
// We also could have written `var users = <User>[
var users = [new User()..name = 'Matan'];
  // OK!
Iterable<String> names = users.map((u) => u.name);
}

void

Lastly, we have void, the newest type in Dart 2. In Dart 1 void was only usable as the return type of a function (such as void main()), but in Dart 2 it has been generalized, and is usable elsewhere, for example Future<void>.

A void type is semantically similar to Object(it could be anything), except with additional restrictions — a void type cannot be used for anything (even == or hashCode), and it is invalid to assign something to a void type:

void foo() {}
void main() {
var bar = foo(); // Invalid
}

In practice, I use void to mean “anything and I don’t care about the elements” or, more commonly, to mean “omitted”, such as in Future<void> or Stream<void>:

/// Clear the cache.
Future<void> purgeCache() { ... }

In the above code snippet, I don’t want users to try and use the return value of the provided Future, as it is not relevant. I’ve seen examples of using Future<Null>for this purpose, and that was actually a workaround before Future<void>was possible.

For example, this is statically OK, but at runtime is invalid in Dart 2:

import 'dart:async';
Future<String> _doAThing() async => 'Test';
Future<Null> doAThing() async => _doAThing();
void main() async {
// Future<String> is not a subtype of type FutureOr<Null>
await doAThing();
}

… where as using Future<void>for doAThing()is valid and correct.

Another example might be a Stream that fires without any event data:

/// Fires an event when a user signs-out of the system.
Stream<void> get onLogOut { ... }

Another more practical use is implementing a class with generic type arguments you won’t be using. For example, implementing the popular Visitor pattern, we can ignore the C(context) type argument when it isn’t used by passing void:

abstract class Visitor<N, C> {
N visitNode(N node, [C context]);
}
class IdentityVisitor<N> extends Visitor<N, void> {
@override
N visitNode(N node, [_]) => node;
}

I hope this brief article helps you with API decisions around using Object, dynamic, void. Leave comments if you have any other questions or ideas!