Optionals in Dart introduce a second state to a variable: where the variable has no value at all, meaning that it contains nothing, or null as it can also be called. null is a special value in Dart just like it in many other programming languages that support nullability or optionals, such as TypeScript, Rust and Swift. In this post we will go through optionals in Dart and what they mean for Dart and Flutter developers alike.

History of NULL Values

Before we get started on learning about nullability and optionality of data types in Dart, let’s take a quick moment to study null or NULL as it is also known in some languages. Tony Hoare, a British computer scientist, first invented the NULL value while working on ALGOL W (a programming language) which was a successor to ALGOL.

NULL values have historically been the cause of major crashes in computer programs in languages such as C since the compilers at that time did not have native support for optional values, unlike the modern compilers today such as Rust or Swift or Dart. Therefore programmers could easily try to access the value of a variable which was set to `NULL` and cause a crash and unwinding of the stack.

Tony Hoare has mentioned that inventing the NULL value was one of his biggest mistakes and you can read more about that on Wikipedia if you are curious. This is partially due to the fact that NULL values have at least partially been found to be responsible for a lot of bugs in software historically that their very existence has been under scrutiny for a long time, hence the reason why many modern compilers today try to support optional data types as we will see in this post.

Data Types vs Optionality

In Dart, variables can be of different data-types, such as Int or String as shown here:

final int age = 10;
final String name = 'John Doe';

Optionality has nothing to do with the data type of a variable itself. Any data type can become optional through appending a question mark to the end of the data type itself as we will soon see.

In these examples, the compiler understands that a variable exists named age that can contain a value that fits into the int data type. It also understands that a variable called name exists that is allowed to contain text / string. If you try to set the value of these variables to null, which is only allowed for nullable values, you will get a compilation error from the compiler:

final int age = null; // this won't compile! 🚨
final String name = null; // neither will this 🚨

In this code, the compiler is aware that the value of null is applicable only to optional variables!

Turning a Data Type to an Optional

Any data type in Dart can become an optional! All you have to do is to suffix the data type with a question mark as shown in the following examples:

final int myAge = 10; // cannot contain null
final int? yourAge = null; // can contain null
final String myName = "John Doe"; // cannot contain null
final String? yourName = null; // can contain null

Just because a data type is nullable, or optional, meaning that it can contain null, doesn’t necessarily mean that it contains null. That’s why there are operators in Dart that can help you turn a data type into nullable or as it is called, optional data type as we will soon see.

In some languages such as Swift, you can make a data type optional as many times as you want, as shown here:

let myAge: Int = 20
let yourAge: Int? = 30
let theirAge: Int?? = 40
let ourAge: Int??? = 50

You would normally not write code like the third and the fourth line of code above but sometimes when processing external data such as JSON values you might encounter situations where you have an optional of an optional, as shown in theirAge value above.

In Dart however, you either have a concrete type such as String or an optional of that type String?; In other words, you cannot have String?? in Dart where the optional String value is itself wrapped in another optional!

Reading Optional Values

Well, now that you have an optional value in some sort of a variable, whether it is a final or a const or a simple var, you’re going to want to read its value. There are various ways of going about this. So let’s start with the absolute worse way of doing this: force unwrapping!

Force unwrapping an optional is the process of reading the optional variable’s contents by force! That means you disregard the fact the variable might contain null and you just expect it to contain a real value. This will crash your application at run-time should the variable contain null, hence it being the worse way of dealing with reading an optional’s value. Let’s how this works:

const String? name = 'Vandad';
final unwrappedName = name!;

You usually don’t want to unwrap your values like this. There are other (safer) ways of unwrapping the value inside your optionals and we will go through them now.

One such way is using the ?? operator which reads the optional variable to its left side, if that variable contains an actual value other than null, it will be picked and used, otherwise, the value to the right hand side will be used. Let’s see an example:

const String? firstName = 'John';
const String lastName = 'Doe';

void testIt() {
  final name = firstName ?? lastName;
  log(name);
}

In this example, the name variable’s data type will be String and not String? since the compiler understands that the ?? operator managed to unwrap the value of either the optional firstName constant or the lastName non-optional String variable so the result must be a valid String since even if firstName was null (which it isn’t), the lastName would be picked making the result a valid String variable.

Here is another example of using the ?? operator:

const String? firstName = null;
const String? lastName = null;

void testIt() {
  final name = firstName ?? lastName;
  log(name ?? 'No name');
}

In this example the data type of name variable will be String? since both firstName and lastName are optionals and actually both set to null making the value inside the name variable certainly a null. What I need you to pay attention to is the log() function here where we are using the ?? operator to either print the String value inside the name variable or the value of 'No name'. This is because the log() function expects a String to print to the debug console and not a String? so we safely unwrap the name variable’s value and if that is null, we will print out 'No name' to the console. Neat!

Another way of safely unwrapping an optional value is using if-statements. If your optional variable is local to the scope, you can simply use an if-statement to make sure it’s not null. If the optional variable is not in the local scope, you need to use a local variable to shadow the global variable. Let’s have a look at an example for the first case:

const String? firstName = null;
const String? lastName = null;

void testIt() {
  final String? name = firstName ?? lastName;
  if (name != null) {
    log(name);
  } else {
    log('name is null');
  }
}

In the above example, after the if-statement, Dart assumes the name variable has the String data-type, and not the optional String? since the if-statement makes that check for you.

In the second case which I mentioned before this code block, if you have an optional that was not created in the local-scope, you will need to shadow that with a local variable which then you can apply the if-statement to. Let me show you how:

const String? firstName = null;
const String? lastName = null;

void testIt() {
  if (firstName != null) {
    // here firstName is still an optional `String?`
  }
  // but if you shadow it with a local variable,
  // it becomes a non-optional `String`
  final name = firstName;
  if (name != null) {
    // here name is a non-optional `String`
    log(name);
  } else {
    // here name is null
    log('Name is null');
  }
}

In the code above, by assigning the firstName global optional constant to the local name variable and checking name against null, we can ensure that in the if-statement’s body the value of name is not null, meaning that we effectively turned String? into a String. This is quite a useful feature of the Dart language which you will most probably come to contact with if you haven’t used it yet.

Optional Assignment Operator

In Dart, if you have an optional variable and want to assign a new value to it only if the variable is null, you can either do the following code:

String? name = 'Foo';
if (name == null) {
  // name will be changed to 'Bar'
  name = 'Bar';
} else {
  // name is not changed
}

This does work but is quite verbose. Thankfully Dart has a shorthand for this kind of operation and that is through the ??= operator which assigns the value to the right hand side of this operator to the left variable only if the value of the variable to the left is null. So let’s rewrite the above code in a more concise way using the ??= operator:

String? name = 'Foo';
name ??= 'Bar';

This way you don’t have to execute any custom if-statements at all. If the name variable contains null, the value of 'Bar' will be assigned to it; but if name contains an actual value before ??=, the value will be retained.

Optional Chaining

Given an optional value, you can execute methods and getters/setters on the optional value using the ?. optional accessor. Check out the following code:

final String? name = 'John Doe';
final length = name.length; // 🚨 compile-error

In the code above we aren’t allowed to access the length getter of String since our data-type for name is not a String, it’s String? (an optional). To solve this problem, you can use the ?. optional accessor as shown here:

final String? name = 'John Doe';
final int? length = name?.length; // length is now int?

Since name is an optional String?, the result of calling the length accessor on it will be an optional int? though the result of the length getter in reality is int. What happens here is that ?. operator executes the accessor/method to the right and returns its result in an optional. If the accessor/function to the right hand side of this operator already returns an optional, the result will not be a double-optional as shown here:

// just a dummy extension which allows us to return
// an optional integer as the length of the string
extension MyLength on String? {
  int? get myLength => this?.length;
}

void testIt() {
  final String? name = 'John Doe';
  final int? length = name?.myLength; // length is now int?
}

Optional chaining, as its name indicates, allows you to continue chaining the results of calling optionals. Imagine that you have a class similar to this:

@immutable
class Person {
  final int? age;
  final Person? mother;
  const Person(this.age, this.mother);
}

Here we have a class called Person that has a recursive connection to itself through the mother property that is an optional person (Person?). This means that every person’s mother is either null (the person’s mother is unknown) or that the person has a mother of type Person. Here is how you would get the age of the person itself and the person’s mother and grand mother ages:

const alice = Person(20, null);
final aliceAge = alice.age;
final aliceMotherAge = alice.mother?.age;
final aliceGrandmotherAge = alice.mother?.mother?.age;

Using optional chaining, you can grab Alice’s grandmother’s age by drilling down the chain of first finding Alice’s mother using alice.mother and then the mother’s mother using ?.mother and at last the mother’s mother’s age using the ?.age accessor.

Null-aware Cascade Operator

The cascade-operator in Swift is really handy and is available in some other languages as well. It is a way to create fluid APIs where the function returns an instance of the object it was performed on. A very basic way of implementing fluid interfaces is to ensure that this is always returned from the function like so:

// please don't do this kind of implementation
// in Dart. Dart's cascade operator already
// provides you with free-fluid-interface-like
// APIs so doing this is just for the purpose
// of this example.
@immutable
class Dog {
  const Dog();

  // this function returns a copy of the current instance
  Dog bark() {
    'Bark'.log();
    return this;
  }

  // this function, too, returns a copy of the current instance
  Dog jump() {
    'Bark'.log();
    return this;
  }
}

void testIt() {
  const fluffers = Dog();
  // this allows us to call bark() and jump() on the same instance
  // in the same statement without having to separating the
  // calls into separate lines
  fluffers.bark().jump();
}

This code does work and allows you to chain method calls to the bark() and the jump() functions since they both return the current instance of the object allowing you to keep calling functions or accessors on the instance. However, with Dart’s cascade operator, your implementation could be done in a much simpler way as shown here:

@immutable
class Dog {
  const Dog();
  void bark() => 'Bark'.log();
  void jump() => 'Jump'.log();
}

void testIt() {
  const fluffers = Dog();
  // the cascade operator ".." allows you to chain
  // method calls
  fluffers
    ..bark()
    ..jump();
}

The null-aware cascade operator ?.. allows you to do the same as you can see above, however, on optional values. Let’s turn the dog into an optional and try this again:

void barkAndJump(Dog? dog) {
  dog
    ?..bark()
    ..jump();
}

As you can see, only the first operator is an optional-cascade-operator since the optional-cascade-operator returns an instance of this at its end automatically if this is available and if not, nothing happens, meaning that the code won’t even be executed. After calling bark() on a valid instance of Dog (meaning that the instance is not-null), the jump() function can be called unconditionally since we are sure we have a valid instance of Dog.

Where to Go From Here

Optionals are an integral part of learning Dart and Flutter as well so you need to practice using them. The Flutter and Dart teams have put together a comprehensive guide to null-safety in Dart which I highly suggest that you read and go through the examples one by one and practice at your own time too.

I’d love to hear from you so feel free to post your comments/thoughts below.