What are generics and phantom types in Move?
November 7, 2026
Generics let you write one struct or function that works with many types, using type parameters in angle brackets: Container<T>, fun new<T>(value: T). Each instantiation, such as Container<u64> or Container<bool>, is a distinct type. A phantom type parameter is one that is used only as a label and never stored in a field, declared as <phantom T>. Phantom parameters are left out when Move works out a struct's abilities, which is why Coin<phantom T> can have key and store even though currency marker types like SUI don't.
Generic structs and functions
module book::box;
public struct Box<T> has store, drop {
value: T,
}
public fun new<T>(value: T): Box<T> {
Box { value }
}
public fun value<T: copy>(b: &Box<T>): T {
b.value
}Type arguments are usually inferred, but you can spell them out:
let a = new(10u8); // Box<u8>
let b = new<u64>(10); // Box<u64>
let c: Box<bool> = new(true);Order matters: Pair<u8, bool> and Pair<bool, u8> are different, incompatible types.
Ability constraints
T: copy, T: store + drop and so on restrict which types can be plugged in. The compiler rejects instantiations that don't meet them:
public fun dup<T: copy>(x: &T): (T, T) { (*x, *x) }Abilities on generic structs are conditional. Box<T> above declares store, drop, but Box<SomeType> only actually has drop if SomeType has drop. (See Move abilities.)
Phantom type parameters
Sometimes a type parameter exists only to tell values apart. A coin's data is just a number, but Coin<USD> and Coin<EUR> must never mix:
module book::money;
public struct Balance<phantom T> has store {
value: u64,
}
public struct USD {}
public struct EUR {}
public fun zero<T>(): Balance<T> { Balance { value: 0 } }
public fun join<T>(a: &mut Balance<T>, b: Balance<T>) {
let Balance { value } = b;
a.value = a.value + value;
}join only accepts two balances of the same currency, checked at compile time. Without phantom, Balance<USD> would only get store if USD had store. You'd have to add spurious abilities to marker types, which weakens them. With phantom, the argument is ignored for ability derivation, so Balance<USD> has store while USD has no abilities at all.
Rules:
- A phantom parameter can appear only in other phantom positions, never as a field type.
struct S<phantom T> { f: T }is an error. - Marking an unused parameter
phantomis not strictly required, but the compiler warns if a parameter could be phantom and isn't. - Ability constraints still apply:
<phantom T: drop>requires the argument to havedrop.
How Sui uses this
The Sui framework relies on phantom types throughout:
Coin<phantom T>andBalance<phantom T>: the coin typeT(for example0x2::sui::SUI) is a label for the currency.TreasuryCap<phantom T>: minting rights for exactly one currency.Table<phantom K, phantom V>: keys and values live in dynamic fields, not in the struct.TransferPolicy<phantom T>andDisplay<phantom T>: per-type configuration owned by the type's Publisher.
Combined with the one-time witness, this gives each currency a unique type that only its defining module can mint.
Generics in transactions
When you call a generic function from a PTB or the CLI, pass the type arguments explicitly:
tx.moveCall({
target: '0x2::coin::value',
typeArguments: ['0x2::sui::SUI'],
arguments: [tx.object(COIN_ID)],
})sui client call --package 0x2 --module coin --function value \
--type-args 0x2::sui::SUI --args <COIN_ID>Limits
Sui's protocol config caps generics: the bytecode verifier limits a single instantiation to 32 type arguments (max_generic_instantiation_length), and a transaction's type arguments are limited to 16 in total, counted recursively (max_type_arguments). Recursive struct definitions are rejected by the compiler.
Sources
- Generics, The Move Book
- Generics, Move Reference
sui-protocol-config, MystenLabs/sui