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.