Generic 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
0x01followed 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
0x00followed by encoded value - Err(error): Byte
0x01followed 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),
});
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());
Size Implications
Option Size
| Value | Encoding | Size |
|---|---|---|
| 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
| Value | Encoding | Size |
|---|---|---|
| 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
- Composite Types - Build complex structures
- Input/Output - Master the I/O system
- Best Practices - Learn encoding best practices