Skip to content

Types

Every value in Tx3 has a static type. Types let the compiler check that datums, redeemers, and parameters fit together before the transaction is ever resolved, and they tell the codegen frontends how to (de)serialize values across the language boundary.

This page is a tour of the type system: the built-in types, the compound types, and the user-defined types you build on top of them. For the literals and operators that produce values of these types, see Data expressions.

TypeWhat it holds
IntA signed integer.
Booltrue or false.
BytesA finite-length byte string. String literals ("abc") and hex literals (0xDEADBEEF) both produce values of type Bytes.
AddressA chain address. The on-wire representation is defined by the target chain.
UtxoRefA reference to a specific UTxO — a (tx_hash, output_index) pair.
AnyAssetA (policy, asset_name, amount) triple representing some quantity of one asset class.

There is no separate String type: text and arbitrary bytes share the Bytes type and are distinguished only by the literal you use to write them.

The unit type is spelled (). It is mostly used as a no-op redeemer where a value is syntactically required but its content is irrelevant:

mint {
amount: MyToken(100),
redeemer: (),
}

List<T> is a homogeneous list of T. Element type is inferred from the first element or from the surrounding context.

type Data {
numbers: List<Int>,
}

Map<K, V> is a key-value mapping with key type K and value type V. In this version of the language every Map literal must contain at least one entry; key and value types are inferred from that first entry.

type Datum {
A: Map<Int, Int>,
}

User-defined types are introduced with the type keyword. There are three shapes.

A record has a fixed set of named fields:

type State {
lock_until: Int,
owner: Bytes,
beneficiary: Bytes,
}

You construct a record with TypeName { field: expr, ... }:

State {
lock_until: 1234567890,
owner: 0xDEADBEEF,
beneficiary: 0x12345678,
}

Field access uses dot notation: state.lock_until.

A variant type is a tagged union with one or more cases. Each case is one of three shapes:

type MyVariant {
Case1 {
field1: Int,
field2: Bytes,
field3: Int,
},
Case2,
}
  • Struct case (Case1 above) — has named fields, like a record case.
  • Unit case (Case2 above) — carries no payload.
  • Tuple case — declared as Case(T1, T2); accepted by the grammar but not constructable in this version of the language.

You construct a variant by naming the type, the case, and (for struct cases) the fields:

MyVariant::Case1 {
field1: 7,
field2: 0xAFAFAF,
field3: 42,
}
MyVariant::Case2 { }

Each case name must be unique within its variant.

A type alias gives an existing type a second name. Aliases are transparent: an alias and the type it points to are interchangeable everywhere.

type AssetName = Bytes;
type PolicyId = Bytes;
type Amount = Int;
type TokenId = PolicyId; // alias chains are allowed
type TokenName = AssetName;
type Balance = Amount;
type Asset {
token_id: TokenId,
token_name: TokenName,
amount: Balance,
}
type TokenBundle = Asset;

Cyclic aliases (type A = B; type B = A;) are rejected.

Two types are equivalent if they are the same primitive, the same List<T> (with equivalent T), the same Map<K, V> (with equivalent K and V), or refer to the same user-defined type after alias chasing.

There are no implicit conversions. Int does not silently turn into Bytes, and Bytes does not silently turn into Address, UtxoRef, or AnyAsset. Where a position expects a specific type, the expression must already have that type.