Skip to content

Type System

LUMOS supports a rich type system that maps cleanly between Rust, TypeScript, Python, Go, and Ruby.

LUMOS generates type-safe code in 5 languages from a single schema. All languages use Borsh serialization for guaranteed cross-language compatibility.

LUMOS TypeRustTypeScriptPythonGoRuby
u8u8numberintuint8Integer
u16u16numberintuint16Integer
u32u32numberintuint32Integer
u64u64numberintuint64Integer
u128u128bigintint[16]byteInteger
i8i8numberintint8Integer
i16i16numberintint16Integer
i32i32numberintint32Integer
i64i64numberintint64Integer
i128i128bigintint[16]byteInteger
boolboolbooleanboolboolTrueClass/FalseClass
StringStringstringstrstringString
PublicKeyPubkeyPublicKeybytes(32)[32]byteArray (32 bytes)
SignatureSignatureUint8Arraybytes(64)[64]byteArray (64 bytes)
Vec<T>Vec<T>T[]list[T][]TArray
Option<T>Option<T>T | undefinedT | None*TT / nil
[T; N][T; N]T[]list[T][N]TArray
LanguageLibraryInstallation
Rustborshcargo add borsh
TypeScript@coral-xyz/borshnpm install @coral-xyz/borsh
Pythonborsh-constructpip install borsh-construct
GoManual (struct tags)Built-in encoding
Rubyborsh-rbgem install borsh-rb

Rust is the primary target for LUMOS. Generated code uses:

  • borsh::BorshSerialize and BorshDeserialize derives
  • Anchor’s Pubkey for Solana types
  • Native Rust primitives
// Generated Rust
use borsh::{BorshSerialize, BorshDeserialize};
use anchor_lang::prelude::*;
#[derive(BorshSerialize, BorshDeserialize)]
pub struct Account {
pub owner: Pubkey, // PublicKey → Pubkey
pub balance: u64, // u64 → u64
pub name: String, // String → String
pub items: Vec<u8>, // Vec<T> → Vec<T>
pub email: Option<String>, // Option<T> → Option<T>
}

TypeScript generation includes both interfaces and Borsh schemas:

// Generated TypeScript
import { PublicKey } from '@solana/web3.js';
import * as borsh from '@coral-xyz/borsh';
export interface Account {
owner: PublicKey; // PublicKey → PublicKey
balance: number; // u64 → number (precision warning)
name: string; // String → string
items: number[]; // Vec<u8> → number[]
email: string | undefined; // Option<String> → string | undefined
}
export const AccountBorshSchema = borsh.struct([
borsh.publicKey('owner'),
borsh.u64('balance'),
borsh.string('name'),
borsh.vec(borsh.u8(), 'items'),
borsh.option(borsh.string(), 'email'),
]);

Python generation uses dataclasses with borsh-construct:

# Generated Python
from dataclasses import dataclass
from typing import Optional, List
from borsh_construct import CStruct, U64, String, Vec, Option, Bytes
@dataclass
class Account:
owner: bytes # 32 bytes for PublicKey
balance: int # u64 → int (Python handles arbitrary precision)
name: str # String → str
items: List[int] # Vec<u8> → List[int]
email: Optional[str] # Option<String> → Optional[str]
AccountSchema = CStruct(
"owner" / Bytes(32),
"balance" / U64,
"name" / String,
"items" / Vec(U8),
"email" / Option(String),
)

Advantages:

  • Python’s int handles arbitrary precision natively (no u64 overflow)
  • borsh-construct provides zero-copy deserialization
  • Type hints for IDE support

Go generation uses struct tags for Borsh encoding:

// Generated Go
package schema
// Account represents the account data
type Account struct {
Owner [32]byte `borsh:"owner"` // PublicKey → [32]byte
Balance uint64 `borsh:"balance"` // u64 → uint64
Name string `borsh:"name"` // String → string
Items []uint8 `borsh:"items"` // Vec<u8> → []uint8
Email *string `borsh:"email"` // Option<String> → *string (nil = None)
}

Key Mappings:

  • PublicKey[32]byte (fixed array)
  • u128[16]byte (Go lacks native 128-bit integers)
  • Option<T>*T (pointer, nil represents None)

Ruby generation uses classes with YARD documentation:

# Generated Ruby
require 'borsh'
# Account represents the account data
# @attr owner [Array<Integer>] The account owner (32 bytes)
# @attr balance [Integer] The account balance
# @attr name [String] The account name
# @attr items [Array<Integer>] The items list
# @attr email [String, nil] Optional email address
class Account
attr_accessor :owner, :balance, :name, :items, :email
SCHEMA = {
owner: [:u8, 32],
balance: :u64,
name: :string,
items: [:array, :u8],
email: [:option, :string]
}
def initialize(opts = {})
@owner = opts[:owner] || Array.new(32, 0)
@balance = opts[:balance] || 0
@name = opts[:name] || ""
@items = opts[:items] || []
@email = opts[:email]
end
end

LUMOS TypeRust TypeTypeScript TypeBorsh Schema
u8u8numberborsh.u8()
u16u16numberborsh.u16()
u32u32numberborsh.u32()
u64u64numberborsh.u64()
u128u128bigintborsh.u128()
i8i8numberborsh.i8()
i16i16numberborsh.i16()
i32i32numberborsh.i32()
i64i64numberborsh.i64()
i128i128bigintborsh.i128()
boolboolbooleanborsh.bool()
StringStringstringborsh.string()
PublicKeyPubkeyPublicKeyborsh.publicKey()
SignatureSignatureUint8Arrayborsh.array(borsh.u8(), 64)
[T]Vec<T>T[]borsh.vec(...)
Option<T>Option<T>T | undefinedborsh.option(...)

#[solana]
struct Numbers {
tiny: u8, // 0 to 255
small: u16, // 0 to 65,535
medium: u32, // 0 to 4,294,967,295
large: u64, // 0 to 18,446,744,073,709,551,615
huge: u128, // 0 to 340,282,366,920,938,463,463,374,607,431,768,211,455
}

Rust output:

pub struct Numbers {
pub tiny: u8,
pub small: u16,
pub medium: u32,
pub large: u64,
pub huge: u128,
}

TypeScript output:

export interface Numbers {
tiny: number;
small: number;
medium: number;
large: number; // May lose precision > Number.MAX_SAFE_INTEGER
huge: bigint;
}

#[solana]
struct SignedNumbers {
tiny: i8, // -128 to 127
small: i16, // -32,768 to 32,767
medium: i32, // -2,147,483,648 to 2,147,483,647
large: i64, // Large signed range
huge: i128, // Massive signed range
}

TypeScript mapping:

  • i8, i16, i32, i64number
  • i128bigint

#[solana]
struct Flags {
is_active: bool,
has_permission: bool,
}

Borsh encoding:

  • true0x01
  • false0x00

#[solana]
struct Metadata {
name: String,
description: String,
symbol: String,
}

Borsh encoding:

  • Length prefix (4 bytes, little-endian)
  • UTF-8 bytes

Example:

  • "hello"0x05000000 + 0x68656c6c6f

Represents a Solana public key (32 bytes).

#[solana]
struct Account {
owner: PublicKey,
authority: PublicKey,
}

Rust mapping:

use anchor_lang::prelude::*;
pub struct Account {
pub owner: Pubkey,
pub authority: Pubkey,
}

TypeScript mapping:

import { PublicKey } from '@solana/web3.js';
export interface Account {
owner: PublicKey;
authority: PublicKey;
}
export const AccountBorshSchema = borsh.struct([
borsh.publicKey('owner'),
borsh.publicKey('authority'),
]);

Borsh encoding:

  • 32 bytes (fixed size)

Represents a Solana signature (64 bytes).

#[solana]
struct Transaction {
signature: Signature,
}

Rust mapping:

pub struct Transaction {
pub signature: Signature, // From solana_program
}

TypeScript mapping:

export interface Transaction {
signature: Uint8Array; // 64 bytes
}
export const TransactionBorshSchema = borsh.struct([
borsh.array(borsh.u8(), 64, 'signature'),
]);

Dynamic-length arrays.

#[solana]
struct Inventory {
items: [PublicKey],
scores: [u64],
}

Rust output:

pub struct Inventory {
pub items: Vec<Pubkey>,
pub scores: Vec<u64>,
}

TypeScript output:

export interface Inventory {
items: PublicKey[];
scores: number[];
}
export const InventoryBorshSchema = borsh.struct([
borsh.vec(borsh.publicKey(), 'items'),
borsh.vec(borsh.u64(), 'scores'),
]);

Borsh encoding:

  • Length prefix (4 bytes, little-endian)
  • Elements sequentially

Example:

  • [1, 2, 3]0x03000000 + 0x01 + 0x02 + 0x03

Nullable fields.

#[solana]
struct Profile {
username: String,
email: Option<String>,
avatar: Option<PublicKey>,
}

Rust output:

pub struct Profile {
pub username: String,
pub email: Option<String>,
pub avatar: Option<Pubkey>,
}

TypeScript output:

export interface Profile {
username: string;
email: string | undefined;
avatar: PublicKey | undefined;
}
export const ProfileBorshSchema = borsh.struct([
borsh.string('username'),
borsh.option(borsh.string(), 'email'),
borsh.option(borsh.publicKey(), 'avatar'),
]);

Borsh encoding:

  • None0x00
  • Some(value)0x01 + value bytes

Example:

  • None0x00
  • Some(42)0x01 + 0x2a

Rust-style enums with three variant types.

#[solana]
enum Status {
Active,
Paused,
Terminated,
}

Rust output:

pub enum Status {
Active,
Paused,
Terminated,
}

TypeScript output:

export type Status =
| { kind: 'Active' }
| { kind: 'Paused' }
| { kind: 'Terminated' };
export const StatusBorshSchema = borsh.rustEnum([
borsh.unit('Active'),
borsh.unit('Paused'),
borsh.unit('Terminated'),
]);

Borsh encoding:

  • Discriminant (1 byte) + variant data
  • Active0x00
  • Paused0x01
  • Terminated0x02

#[solana]
enum Event {
UserJoined(PublicKey),
ScoreUpdated(PublicKey, u64),
GameEnded(PublicKey, u64, i64),
}

Rust output:

pub enum Event {
UserJoined(Pubkey),
ScoreUpdated(Pubkey, u64),
GameEnded(Pubkey, u64, i64),
}

TypeScript output:

export type Event =
| { kind: 'UserJoined'; fields: [PublicKey] }
| { kind: 'ScoreUpdated'; fields: [PublicKey, number] }
| { kind: 'GameEnded'; fields: [PublicKey, number, number] };
export const EventBorshSchema = borsh.rustEnum([
borsh.tuple([borsh.publicKey()], 'UserJoined'),
borsh.tuple([borsh.publicKey(), borsh.u64()], 'ScoreUpdated'),
borsh.tuple([borsh.publicKey(), borsh.u64(), borsh.i64()], 'GameEnded'),
]);

#[solana]
enum Instruction {
Initialize {
authority: PublicKey,
max_users: u32,
},
UpdateConfig {
new_authority: PublicKey,
},
Terminate,
}

Rust output:

pub enum Instruction {
Initialize {
authority: Pubkey,
max_users: u32,
},
UpdateConfig {
new_authority: Pubkey,
},
Terminate,
}

TypeScript output:

export type Instruction =
| { kind: 'Initialize'; authority: PublicKey; max_users: number }
| { kind: 'UpdateConfig'; new_authority: PublicKey }
| { kind: 'Terminate' };
export const InstructionBorshSchema = borsh.rustEnum([
borsh.struct([
borsh.publicKey('authority'),
borsh.u32('max_users'),
], 'Initialize'),
borsh.struct([
borsh.publicKey('new_authority'),
], 'UpdateConfig'),
borsh.unit('Terminate'),
]);

The following types are NOT supported:

❌ Floating point (f32, f64) ❌ Fixed-size arrays ([u8; 32]) - use Vec or PublicKey ❌ Tuples ((u64, String)) - use structs instead ❌ References (&str, &[u8]) ❌ Raw pointers (*const, *mut) ❌ Function pointers

Fixed-size arrays → Vec:

// ❌ Not supported
struct Data {
buffer: [u8; 1024],
}
// ✅ Use Vec instead
struct Data {
buffer: [u8], // Vec<u8>
}

Tuples → Struct:

// ❌ Not supported
struct Pair {
data: (u64, String),
}
// ✅ Use struct instead
struct Pair {
first: u64,
second: String,
}