JSPM

@solana/options

2.0.0-preview.3.20240509011034.d525f9118aa77b20276084bca3efd26c2c7882df
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 800336
  • Score
    100M100P100Q184461F
  • License MIT

Managing and serializing Rust-like Option types in JavaScript

Package Exports

  • @solana/options

Readme

npm npm-downloads semantic-release
code-style-prettier

@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@experimental.

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.

// Create an option with a value.
some('Hello World');
some<number | string>(123);

// Create an empty option.
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')); // true
isSome(none()); // false

isNone(some('Hello World')); // false
isNone(none()); // true

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'); // Some<string>
wrapNullable(null); // None

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')); // "Hello World"
unwrapOption(none()); // null

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'); // "Hello World"
unwrapOption(none(), () => 'Default'); // "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')))); // "Hello World"
unwrapOptionRecursively(some(some(none<string>()))); // null

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() }],
});
// { a: "hello", b: null, c: [{ c1: 42 }, { c2: null }] }

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',
);
// { a: "hello", b: "Default", c: [{ c1: 42 }, { c2: "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>. It stores whether or not the item exists as a boolean prefix using a u8 by default.

const optionStringCodec = getOptionCodec(addCodecSizePrefix(getUtf8Codec(), getU32Codec()));

optionStringCodec.encode(some('Hi'));
// 0x01020000004869
//   | |       └-- utf8 string content ("Hi").
//   | └-- u32 string prefix (2 characters).
//   └-- 1-byte prefix (Some).

optionStringCodec.encode(none());
// 0x00
//   └-- 1-byte prefix (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(addCodecSizePrefix(getUtf8Codec(), getU32Codec()), {
    prefix: getU32Codec(),
});

u32OptionStringCodec.encode(some('Hi'));
// 0x01000000020000004869
//   └------┘ 4-byte prefix (Some).

u32OptionStringCodec.encode(none());
// 0x00000000
//   └------┘ 4-byte prefix (None).

Additionally, if the item is a FixedSizeCodec, you may set the fixed option to true to also make the returned option codec a FixedSizeCodec. To do so, it will pad null values with zeroes to match the length of existing values.

const fixedOptionStringCodec = getOptionCodec(
    fixCodecSize(getUtf8Codec(), 8), // Only works with fixed-size items.
    { fixed: true },
);

fixedOptionStringCodec.encode(some('Hi'));
// 0x014869000000000000
//   | └-- 8-byte utf8 string content ("Hi").
//   └-- 1-byte prefix (Some).

fixedOptionStringCodec.encode(none());
// 0x000000000000000000
//   | └-- 8-byte of padding to make a fixed-size codec.
//   └-- 1-byte prefix (None).

Separate getOptionEncoder and getOptionDecoder functions are also available.

const bytes = getOptionEncoder(getU32Encoder()).encode(some(42));
const value = getOptionDecoder(getU32Decoder()).decode(bytes);

Zeroable option codec

Similarly to the getOptionCodec function, The getZeroableOptionCodec function accepts a codec of type T and returns a codec of type Option<T>. However, instead of relying on a boolean prefix to determine whether the item exists, it uses a zero value to represent None. This means, you may only use this codec with fixed-size items.

const codec = getZeroableOptionCodec(getU16Codec());
codec.encode(some(42)); // 0x2a00
codec.encode(none()); // 0x0000
codec.decode(new Uint8Array([42, 0])); // Some<42>
codec.encode(new Uint8Array([0, 0])); // None

As you can see, by default, it uses a Uint8Array of zeroes to represent None. However, you may provide a custom zero value that will be used to encode/decode None values. Note that this zero value must be a Uint8Array of the same length as the codec's fixed size.

const codec = getZeroableOptionCodec(getU16Codec(), {
    zeroValue: new Uint8Array([255, 255]),
});
codec.encode(some(42)); // 0x2a00
codec.encode(none()); // 0xfffff
codec.encode(new Uint8Array([0, 0])); // Some<0>
codec.decode(new Uint8Array([42, 0])); // Some<42>
codec.decode(new Uint8Array([255, 255])); // None

Separate getZeroableOptionEncoder and getZeroableOptionDecoder functions are also available.

const bytes = getZeroableOptionEncoder(getU16Encoder()).encode(some(42));
const value = getZeroableOptionDecoder(getU16Decoder()).decode(bytes);

To read more about the available codecs and how to use them, check out the documentation of the main @solana/codecs package.