suideveloper
All questions
Sui Move

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

Move
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:

Move
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:

Move
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:

Move
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 phantom is 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 have drop.

How Sui uses this

The Sui framework relies on phantom types throughout:

  • Coin<phantom T> and Balance<phantom T>: the coin type T (for example 0x2::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> and Display<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:

TypeScript
tx.moveCall({
  target: '0x2::coin::value',
  typeArguments: ['0x2::sui::SUI'],
  arguments: [tx.object(COIN_ID)],
})
Terminal
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

More on sui move