@solana/options
This package allows us to manage and serialize Rust-like Option types in JavaScript. It can be used standalone, but it is also exported as part of the Solana JavaScript SDK @solana/web3.js@rc
.
This package is also part of the @solana/codecs
package which acts as an entry point for all codec packages as well as for their documentation.
Creating options
In Rust, we define optional values as an Option<T>
type which can either be Some(T)
or None
. This is usually represented as T | null
in the JavaScript world. The issue with this approach is it doesn't work with nested options. For instance, an Option<Option<T>>
in Rust would become a T | null | null
in JavaScript which is equivalent to T | null
. That means, there is no way for us to represent the Some(None)
value in JavaScript or any other nested option.
To solve this issue, this library provides an Option<T>
union type that works very similarly to the Rust Option<T>
type. It is defined as follows:
type Option<T> = Some<T> | None;
type Some<T> = { __option: 'Some'; value: T };
type None = { __option: 'None' };
To improve the developer experience, helper functions are available to help you create options. The type T
of the option can either be inferred by TypeScript or explicitly provided.
some('Hello World');
some<number | string>(123);
none();
none<number | string>();
Option helpers
This library also provides helper functions to help us identify and manage Option
types.
For instance, you can use the isSome
and isNone
type guards to check whether a given Option
is of the desired type.
isSome(some('Hello World'));
isSome(none());
isNone(some('Hello World'));
isNone(none());
If you are given a type T | null
, you may also use the wrapNullable
helper function to transform it into an Option<T>
type.
wrapNullable('Hello world');
wrapNullable(null);
Unwrapping options
Several helpers are available to help you unwrap your options and access their potential value. For instance, the unwrapOption
function transforms an Option<T>
type into T
if the value exits and null
otherwise.
unwrapOption(some('Hello World'));
unwrapOption(none());
If null
isn’t the value you want to use for None
options, you may provide a custom fallback function as the second argument. Its return value will be assigned to None
options.
unwrapOption(some('Hello World'), () => 'Default');
unwrapOption(none(), () => 'Default');
Note that this unwrapOption
function does not recursively unwrap nested options. You may use the unwrapOptionRecursively
function for that purpose instead.
unwrapOptionRecursively(some(some(some('Hello World'))));
unwrapOptionRecursively(some(some(none<string>())));
The unwrapOptionRecursively
function also walks any object and array it encounters and recursively unwraps any option it identifies in its journey without mutating any object or array.
unwrapOptionRecursively({
a: 'hello',
b: none(),
c: [{ c1: some(42) }, { c2: none() }],
});
The unwrapOptionRecursively
also accepts a fallback function as a second argument to provide custom values for None
options.
unwrapOptionRecursively(
{
a: 'hello',
b: none(),
c: [{ c1: some(42) }, { c2: none() }],
},
() => 'Default',
);
Option codec
The getOptionCodec
function behaves exactly the same as the getNullableCodec
except that it encodes Option<T>
types instead of T | null
types.
Namely, it accepts a codec of type T
and returns a codec of type Option<T>
. Note that, when encoding, T
or null
may also be provided directly as input and will be interpreted as Some(T)
or None
respectively. However, when decoding, the output will always be an Option<T>
type.
It stores whether or not the item exists as a boolean prefix using a u8
by default.
const stringCodec = addCodecSizePrefix(getUtf8Codec(), getU32Codec());
getOptionCodec(stringCodec).encode('Hi');
getOptionCodec(stringCodec).encode(some('Hi'));
getOptionCodec(stringCodec).encode(null);
getOptionCodec(stringCodec).encode(none());
You may provide a number codec as the prefix
option of the getOptionCodec
function to configure how to store the boolean prefix.
const u32OptionStringCodec = getOptionCodec(stringCodec, {
prefix: getU32Codec(),
});
u32OptionStringCodec.encode(some('Hi'));
u32OptionStringCodec.encode(none());
Additionally, if the item is a FixedSizeCodec
, you may set the noneValue
option to "zeroes"
to also make the returned Option codec a FixedSizeCodec
. To do so, it will pad None
values with zeroes to match the length of existing values.
const codec = getOptionCodec(
fixCodecSize(getUtf8Codec(), 8),
{ noneValue: 'zeroes' },
);
codec.encode(some('Hi'));
codec.encode(none());
The noneValue
option can also be set to an explicit byte array to use as the padding for None
values. Note that, in this case, the returned codec will not be a FixedSizeCodec
as the byte array representing None
values may be of any length.
const codec = getOptionCodec(getUtf8Codec(), {
noneValue: new Uint8Array([255]),
});
codec.encode(some('Hi'));
codec.encode(none());
Last but not least, the prefix
option of the getOptionCodec
function can also be set to null
, meaning no prefix will be used to determine whether the item exists. In this case, the codec will rely on the noneValue
option to determine whether the item is None
.
const codecWithZeroNoneValue = getOptionCodec(getU16Codec(), {
noneValue: 'zeroes',
prefix: null,
});
codecWithZeroNoneValue.encode(some(42));
codecWithZeroNoneValue.encode(none());
const codecWithCustomNoneValue = getOptionCodec(getU16Codec(), {
noneValue: new Uint8Array([255]),
prefix: null,
});
codecWithCustomNoneValue.encode(some(42));
codecWithCustomNoneValue.encode(none());
Finally, note that if prefix
is set to null
and no noneValue
is provided, the codec assume that the item exists if and only if some remaining bytes are available to decode. This could be useful to describe data structures that may or may not have additional data to the end of the buffer.
const codec = getOptionCodec(getU16Codec(), { prefix: null });
codec.encode(some(42));
codec.encode(none());
codec.decode(new Uint8Array([42, 0]));
codec.decode(new Uint8Array([]));
To recap, here are all the possible configurations of the getOptionCodec
function, using a u16
codec as an example.
encode(some(42)) / encode(none()) | No noneValue (default) | noneValue: "zeroes" | Custom noneValue (0xff ) |
---|
u8 prefix (default) | 0x012a00 / 0x00 | 0x012a00 / 0x000000 | 0x012a00 / 0x00ff |
Custom prefix (u16 ) | 0x01002a00 / 0x0000 | 0x01002a00 / 0x00000000 | 0x01002a00 / 0x0000ff |
No prefix | 0x2a00 / 0x | 0x2a00 / 0x0000 | 0x2a00 / 0xff |
Separate getOptionEncoder
and getOptionDecoder
functions are also available.
const bytes = getOptionEncoder(getU32Encoder()).encode(some(42));
const value = getOptionDecoder(getU32Decoder()).decode(bytes);
To read more about the available codecs and how to use them, check out the documentation of the main @solana/codecs
package.