Polkadart Logo
SCALE Codec

Generic Types

Working with Option, Result, and other generic SCALE types

Generic Types Overview

SCALE provides generic wrapper types that add semantics to values. These types are essential for representing optional values, error handling, and nullable data in a type-safe way.

Option Type

The Option type represents a value that may or may not be present - similar to nullable types but with explicit SCALE encoding.

Basic Option Usage

import 'package:polkadart_scale_codec/polkadart_scale_codec.dart';

// Option<u32>
final codec = OptionCodec(U32Codec.codec);

// Encoding Some(value)
final output1 = ByteOutput();
codec.encodeTo(42, output1);
print(output1.toBytes()); // [1, 42, 0, 0, 0]
//                            └┘  └────┬────┘
//                          Some    value

// Encoding None
final output2 = ByteOutput();
codec.encodeTo(null, output2);
print(output2.toBytes()); // [0]
//                            └┘
//                          None

Encoding Rules

Option is encoded as:

  • None: Single byte 0x00
  • Some(value): Byte 0x01 followed by encoded value
final codec = OptionCodec(U32Codec.codec);

// None
codec.encodeTo(null, output);     // [0]

// Some(100)
codec.encodeTo(100, output);      // [1, 100, 0, 0, 0]

// Some(0) - note: different from None!
codec.encodeTo(0, output);        // [1, 0, 0, 0, 0]

Decoding Options

final codec = OptionCodec(U32Codec.codec);

// Decode Some
final input1 = Input.fromHex('0x0164000000');
final value1 = codec.decode(input1);
print(value1); // 100

// Decode None
final input2 = Input.fromHex('0x00');
final value2 = codec.decode(input2);
print(value2); // null

Option with Complex Types

// Option<(u32, bool)>
final tupleCodec = TupleCodec([U32Codec.codec, BoolCodec.codec]);
final optionCodec = OptionCodec(tupleCodec);

// Some
final output = ByteOutput();
optionCodec.encodeTo([42, true], output);
// [1, 42, 0, 0, 0, 1]

// None
optionCodec.encodeTo(null, output);
// [0]

Option Class

The package also provides an Option class for more explicit handling:

// Using Option class
final some = Option.some(42);
final none = Option.none();

print(some.isSome); // true
print(some.isNone); // false
print(some.value);  // 42

print(none.isSome); // false
print(none.isNone); // true

Nested Options

For nested options, use NestedOptionCodec:

// Option<Option<u32>>
final codec = NestedOptionCodec(OptionCodec(U32Codec.codec));

// None
codec.encodeTo(Option.none(), output); // [0]

// Some(None)
codec.encodeTo(Option.some(null), output); // [1, 0]

// Some(Some(42))
codec.encodeTo(Option.some(42), output); // [1, 1, 42, 0, 0, 0]

Result Type

The Result type represents either success (Ok) or failure (Err) - similar to Rust's Result or functional programming's Either.

Basic Result Usage

// Result<u32, String>
final codec = ResultCodec(
  U32Codec.codec,      // Ok type
  StrCodec.codec,      // Err type
);

// Encoding Ok(value)
final output1 = ByteOutput();
codec.encodeTo(Result.ok(42), output1);
print(output1.toBytes()); // [0, 42, 0, 0, 0]
//                            └┘  └────┬────┘
//                           Ok     value

// Encoding Err(error)
final output2 = ByteOutput();
codec.encodeTo(Result.err('error'), output2);
print(output2.toBytes()); // [1, 20, 101, 114, 114, 111, 114]
//                            └┘  └─────────┬─────────────┘
//                          Err      "error" string

Encoding Rules

Result is encoded as:

  • Ok(value): Byte 0x00 followed by encoded value
  • Err(error): Byte 0x01 followed by encoded error

Decoding Results

final codec = ResultCodec(U32Codec.codec, StrCodec.codec);

// Decode Ok
final input1 = Input.fromHex('0x0064000000');
final result1 = codec.decode(input1);
print(result1.isOk);      // true
print(result1.okValue);   // 100

// Decode Err
final input2 = Input.fromHex('0x01146661696c6564');
final result2 = codec.decode(input2);
print(result2.isErr);     // true
print(result2.errValue);  // "failed"

Result Class

The Result class provides type-safe access:

// Create results
final success = Result.ok(42);
final failure = Result.err('Something went wrong');

// Check status
if (success.isOk) {
  print('Value: ${success.okValue}');
}

if (failure.isErr) {
  print('Error: ${failure.errValue}');
}

// Convert to JSON
print(success.toJson()); // {Ok: 42}
print(failure.toJson()); // {Err: "Something went wrong"}

Complex Result Types

// Result<{id: u32, name: String}, {code: u8, message: String}>
final okCodec = CompositeCodec({
  'id': U32Codec.codec,
  'name': StrCodec.codec,
});

final errCodec = CompositeCodec({
  'code': U8Codec.codec,
  'message': StrCodec.codec,
});

final codec = ResultCodec(okCodec, errCodec);

// Success case
final success = Result.ok({
  'id': 1,
  'name': 'Alice',
});

// Error case
final error = Result.err({
  'code': 404,
  'message': 'Not found',
});

final encoded = codec.encode(success);

Common Patterns

Optional Fields in Structs

// User with optional email
final userCodec = CompositeCodec({
  'id': U32Codec.codec,
  'name': StrCodec.codec,
  'email': OptionCodec(StrCodec.codec),
  'age': OptionCodec(U8Codec.codec),
});

final user1 = {
  'id': 1,
  'name': 'Alice',
  'email': '[email protected]',
  'age': 30,
};

final user2 = {
  'id': 2,
  'name': 'Bob',
  'email': null,  // No email
  'age': null,     // Age not provided
};

final encoded1 = userCodec.encode(user1);
final encoded2 = userCodec.encode(user2);

API Response Pattern

// API responses as Result<Data, Error>
class ApiResponse {
  static final codec = ResultCodec(
    // Success: {data: Vec<u8>}
    CompositeCodec({
      'data': U8SequenceCodec.codec,
    }),
    // Error: {code: u16, message: String}
    CompositeCodec({
      'code': U16Codec.codec,
      'message': StrCodec.codec,
    }),
  );
}

void handleResponse(Uint8List encoded) {
  final result = ApiResponse.codec.decode(Input.fromBytes(encoded));

  if (result.isOk) {
    final data = result.okValue!['data'];
    print('Success: $data');
  } else {
    final code = result.errValue!['code'];
    final message = result.errValue!['message'];
    print('Error $code: $message');
  }
}

Nullable Collections

// Option<Vec<u32>>
final codec = OptionCodec(SequenceCodec(U32Codec.codec));

// Some list
codec.encodeTo([1, 2, 3], output);

// Empty list (different from None!)
codec.encodeTo([], output);  // [1, 0] - Some([])

// None
codec.encodeTo(null, output); // [0]

Complete Example: Database Record

import 'package:polkadart_scale_codec/polkadart_scale_codec.dart';

class DatabaseRecord {
  final int id;
  final String name;
  final int? age;           // Optional
  final String? email;      // Optional
  final Result<String, String> status; // Ok or Err

  DatabaseRecord(this.id, this.name, this.age, this.email, this.status);
}

class DatabaseRecordCodec with Codec<DatabaseRecord> {
  const DatabaseRecordCodec();

  @override
  void encodeTo(DatabaseRecord value, Output output) {
    U32Codec.codec.encodeTo(value.id, output);
    StrCodec.codec.encodeTo(value.name, output);
    OptionCodec(U8Codec.codec).encodeTo(value.age, output);
    OptionCodec(StrCodec.codec).encodeTo(value.email, output);
    ResultCodec(StrCodec.codec, StrCodec.codec).encodeTo(value.status, output);
  }

  @override
  DatabaseRecord decode(Input input) {
    return DatabaseRecord(
      U32Codec.codec.decode(input),
      StrCodec.codec.decode(input),
      OptionCodec(U8Codec.codec).decode(input),
      OptionCodec(StrCodec.codec).decode(input),
      ResultCodec(StrCodec.codec, StrCodec.codec).decode(input),
    );
  }

  @override
  bool isSizeZero() => false;
}

void main() {
  // Active user with all fields
  final record1 = DatabaseRecord(
    1,
    'Alice',
    30,
    '[email protected]',
    Result.ok('Active'),
  );

  // Inactive user with missing fields
  final record2 = DatabaseRecord(
    2,
    'Bob',
    null,  // Age not provided
    null,  // Email not provided
    Result.err('Account suspended'),
  );

  final codec = DatabaseRecordCodec();

  // Encode both
  final encoded1 = codec.encode(record1);
  final encoded2 = codec.encode(record2);

  print('Record 1 size: ${encoded1.length} bytes');
  print('Record 2 size: ${encoded2.length} bytes');

  // Decode and verify
  final decoded1 = codec.decode(Input.fromBytes(encoded1));
  print('Name: ${decoded1.name}, Age: ${decoded1.age}');

  if (decoded1.status.isOk) {
    print('Status: ${decoded1.status.okValue}');
  }
}

When to Use What

Use Option When:

Optional Fields

Fields that may not always be present

final codec = CompositeCodec({
  'value': OptionCodec(U32Codec.codec),
});

Nullable Values

Values that can legitimately be null

final codec = OptionCodec(StrCodec.codec);

Sparse Data

Arrays where most elements are None

final codec = SequenceCodec(OptionCodec(U32Codec.codec));

Use Result When:

Error Handling

Operations that can succeed or fail

final codec = ResultCodec(DataCodec(), ErrorCodec());

API Responses

Network responses with success/error states

final codec = ResultCodec(SuccessCodec(), ErrorCodec());

Validation

Validated vs invalid data

final codec = ResultCodec(ValidCodec(), ValidationErrorCodec());

Size Implications

Option Size

ValueEncodingSize
None[0]1 byte
Some(0)[1, 0, 0, 0, 0]5 bytes (u32)
Some(42)[1, 42, 0, 0, 0]5 bytes (u32)

Rule: 1 byte overhead for the tag

Result Size

ValueEncodingSize
Ok(42)[0, 42, 0, 0, 0]5 bytes
Err("fail")[1, 16, 102, 97, 105, 108]6 bytes

Rule: 1 byte overhead for the tag

Best Practices

Use Dart Nullables for Option

For simple cases, OptionCodec<T> works with Dart's nullable types:
final codec = OptionCodec(U32Codec.codec);
int? value = 42;  // Can be null
codec.encodeTo(value, output);

Use Result for Explicit Error Handling

Result provides better error context than throwing exceptions:
// Good
Result<User, ValidationError> validate(UserInput input);

// Less good
User validate(UserInput input); // throws on error

Don't Confuse Option with Default Values

// Option<u32>: None vs Some(0) are different!
OptionCodec(U32Codec.codec).encodeTo(null, output); // [0]
OptionCodec(U32Codec.codec).encodeTo(0, output);    // [1, 0, 0, 0, 0]

Nested Options Need Special Handling

// For Option<Option<T>>, use NestedOptionCodec
final codec = NestedOptionCodec(OptionCodec(U32Codec.codec));

Next Steps