Skip to content

Optionals

As mentioned in the type system overview, all primitive types, structs, and message structs can be made nullable. That is, variables, function parameters, contract parameters and structure fields of primitive or struct types can hold the special null value that represents the intentional absence of any other value.

Such data types that may or may not contain the null value are called optionals.

You can make a primitive or struct type into an optional by adding a question mark ? after its type declaration.

struct StOpt {
// Optionals as struct fields
opt: Int?; // Int or null
}
message MsOpt {
// Optionals as message fields
opt: StOpt?; // notice how the struct StOpt is used in this definition
}
contract Optionals(
// Optionals as contract parameters
opt: Int?,
address: Address?,
) {
// Optionals as function parameters
fun reset(opt: Int?) {
self.opt = opt;
self.address = null; // explicit null value
}
receive(msg: MsOpt) {
// Optionals as local variables
let opt: Int? = 12;
// Explicit check of the message struct field
if (msg.opt != null) {
// Non-null assertion to work with its inner value
self.reset(msg.opt!!.opt);
}
}
}

Since map<K, V> and bounced<Msg> are not primitive or struct types, they cannot be made optional. Furthermore, their inner key-value types (in the case of a map) and the inner message struct (in the case of a bounced constructor) cannot be optional too.

// COMPILATION ERROR! Map key types cannot be optional
let myMap: map<Int?, Int> = emptyMap();
// ~~~~

Creating a nested optional type by adding multiple question marks ? is not allowed, as optionals are neither a primitive nor a struct type.

// COMPILATION ERROR! Nested optional types are not allowed
fun invalidNestedOptional(a: Int??) {}
// ~~~~~

Optional fields of structures that are not defined implicitly hold the null value by default. That said, optionals as local variables as optionals require initialization.

struct StOpt {
// Defaults to null
nullDef: Int?;
}
fun locVar() {
// Requires an initial value: either null or a value of the Int type
let mayBeeBayBee: Int? = null;
}

When initializing a new local variable to null in the let statement, you must explicitly provide the type ascription as it cannot be inferred.

let opt: Int? = null;
let myMap: map<Int, Int> = emptyMap(); // = null, since empty maps are nulls

You can assign the current value of one optional to another if their types match. However, to access the non-null value of an optional in an expression, you must use the non-null assertion operator !!.

Attempts to assign or directly access an optional value in an expression will result in a compilation error.

let opt1: Int? = 42;
let opt2: Int? = 378;
opt1 = opt2; // 378
let notOpt: Int = 42;
notOpt = opt2!!; // opt2 isn't null, so notOpt is 378
notOpt = opt2; // COMPILATION ERROR! Type mismatch

To access the non-null value of an optional in an expression, you must use the non-null assertion operator !! to unwrap the value. If you are sure the value is not null at the time of the assertion, use the !! operator directly, without prior if...else checks.

In the general case it is better to explicitly check for null before asserting its absence. Otherwise, when the value is null, assertions with !! operator will result in a compilation error if the compiler can track it at compile-time, or, if it cannot, in an exception with exit code 128: Null reference exception.

fun misplacedCourage(opt: Int?) {
// `opt` could be null, and the following assertion could throw an exit code 128:
dump(opt!!);
}

Serialization

When serialized to a Cell or to contract’s persistent state, optionals occupy no less than one bit and, at most, one bit on top of the size of the wrapped type.

That is, if their value is null, only a single 0 bit is stored in a Builder. Otherwise, a single 1 bit is stored, followed by the non-null value.

Deserialization works inversely. First, a single bit is loaded from a Slice. If it is 0, the value is read as null. If it is 1, then the value is loaded from the following data bits.

struct Wasp {
hasSting: Bool?, // 2 bits max: 1 for the optional, 0 or 1 for the boolean
stingLength: Int?, // 258 bits max: 1 for the optional, 0 or 257 for the integer
}

Optionals can have serialization annotations provided after the as keyword and have all the same serialization options as their encapsulated primitive or struct types. As such, the optional Int? type has the most serialization formats available.

contract IntResting(
// Persistent state variables (contract parameters)
maybeOneByte: Int? as int8, // takes either 1 (when null) or 9 (when not null) bits
maybeTwoBytes: Int? as int16, // takes either 1 or 17 bits
maybeCoins: Int? as coins, // takes either 1 or up to 125 bits, depending on the value
) {
// ...
}