Branded types
Identity, Seed, TxHash, Base64, HexPublicKey — compile-time branded strings that prevent mixing up incompatible values.
TypeScript treats all strings as structurally equivalent. Without branding, the compiler happily accepts buildTransaction({ destination: seedString }) — the wrong type in the wrong slot, and there's no error until runtime.
Branded types fix this by attaching a phantom property that only exists in the type system:
type Identity = string & { readonly __brand: "Identity" }The brand has zero runtime cost. It only affects the compiler. The value itself is still a plain string — but now Seed and Identity are distinct types, and swapping them is a compile error.
The bug brands prevent
Without brands:
// Both are `string` — compiler sees no problem
async function transfer(from: string, to: string, amount: bigint) { ... }
const seed = "aaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
const identity = "CFBMEMZOIDEXQAUXYYSZIURADQLAPWPMNJXQSNVQZAHYVOPYUKKJBJUCTVJL"
// Arguments swapped — compiles fine, fails at runtime or worse: silently corrupts the transaction
await transfer(identity, seed, 1000n)With brands:
async function transfer(from: Seed, to: Identity, amount: bigint) { ... }
// Type error: Argument of type 'Identity' is not assignable to parameter of type 'Seed'
await transfer(identity, seed, 1000n)The compiler catches the swap before the code ever runs.
Types
| Type | Format | Description |
|---|---|---|
Identity | 60 uppercase chars | Qubic address derived from a public key |
Seed | 55 lowercase chars | Private key material — treat like a password |
TxHash | 60 lowercase chars | Transaction identifier |
Base64 | standard base64 | Encoded transaction payload for RPC broadcast |
HexPublicKey | 64 hex chars | 32-byte FourQ public key as a lowercase hex string |
Getting branded values
You can't just write const id: Identity = someString — that's a type error. Use a constructor or type guard to validate and produce a branded value:
import { toIdentity, toSeed, toTxHash, toBase64 } from "@qubic.org/types"
import { isIdentity } from "@qubic.org/types"
// Constructor: validates and brands, throws on bad input
const id = toIdentity(req.body.destination)
const seed = toSeed(process.env.WALLET_SEED!)
// Guard: validates and narrows, returns boolean
if (isIdentity(raw)) {
// raw: Identity inside this block
}For values you know are valid at compile time (e.g. hardcoded test fixtures), use an assertion:
const testIdentity = "CFBMEMZOIDEXQAUXYYSZIURADQLAPWPMNJXQSNVQZAHYVOPYUKKJBJUCTVJL" as IdentityThis skips validation entirely — only use it where you control the value.
HexPublicKey
HexPublicKey is distinct from Identity. Both represent a public key, but in different encodings:
Identity— the Qubic-specific 60-char base-26 encoding, used in wallet addresses and transfer destinationsHexPublicKey— 64 lowercase hex characters, the raw bytes of the 32-byte FourQ public key as hex
They are not interchangeable. Passing a HexPublicKey where an Identity is expected is a compile error.