beginner
+150 XP

Types & Data: Your Building Blocks

Everything in your smart contract is built from types — numbers, text, lists, and more. This lesson shows you what types Move gives you, how to pick the right one, and how to avoid common traps. No prior type-system knowledge needed.

Lesson Syllabus

Integer Types & Casting
🔢

The Integer Family

Let's start with numbers — the most common type in any smart contract. Move gives you six sizes of unsigned integers ("unsigned" just means they can't be negative — no minus signs allowed). Think of it like shoe sizes vs. distances: you don't need the same measuring tool for both. - **u8** (1 byte): Holds 0-255. Perfect for tiny values like someone's age or a percentage. - **u16** (2 bytes): Holds 0-65,535. Good for slightly bigger bounded values. - **u32** (4 bytes): Holds 0 to about 4.3 billion. - **u64** (8 bytes): Holds 0 to about 18.4 quintillion. This is the workhorse — use it for balances, timestamps, and counters. - **u128** (16 bytes): Really big numbers for DeFi math where u64 might overflow. - **u256** (32 bytes): Enormous numbers for cryptographic operations. Why so many sizes? Efficiency and safety. If you know a value will never exceed 255, using u8 communicates that intent clearly. But when in doubt, u64 is your safe default.

🔄

Type Casting with as

Here's something that trips up beginners: Move will NOT automatically convert between number types for you. If you have a u8 and need a u64, you have to say so explicitly using the `as` keyword. It's like converting inches to centimeters — you have to be intentional about it. This comes in two flavors: - **Widening** (small to big, like u8 to u64): Always safe. The value fits easily in the bigger container. - **Narrowing** (big to small, like u64 to u8): Dangerous! If the value is too big to fit, your transaction crashes immediately. Trying to squeeze 500 into a u8 (max 255) is like trying to pour a gallon into a cup. The safe pattern: do your math in a bigger type first, then narrow down only when you're sure the result fits.

🛡️

Arithmetic & Overflow Safety

Here's one of Move's best safety features: it automatically checks your math at runtime. If you add two numbers and the result is too big for the type, the transaction aborts. If you subtract and would get a negative number, it aborts (remember, no negative numbers in Move!). In older smart contract languages like Solidity (before version 0.8), overflow would silently "wrap around" — meaning `255 + 1` would become `0`. That caused millions of dollars in bugs. Move says "nope" and just stops the transaction, which is much safer. The tradeoff: you need to think about whether your math might overflow, and use a bigger type if it could.

Vectors, Options & Strings
📚

vector<T>: Dynamic Arrays

Think of a `vector` as a shopping list you can add to and remove from. It's Move's dynamic array — a growable, ordered collection of items that are all the same type. The `<T>` part just means "of what" — `vector<u64>` is a list of numbers, `vector<address>` is a list of wallet addresses, `vector<u8>` is a list of bytes (which is also how Move stores raw text!). You can create one empty and fill it up, or start it pre-loaded with items. The key operations are: `push_back` (add to the end), `pop_back` (remove from the end), `borrow` (peek at an element), and `length` (how many items). Vectors are zero-indexed, meaning the first item is at position 0, the second at position 1, and so on.

Option<T>: Maybe a Value

Imagine a box that might be empty. You can't just reach in blindly — you have to check first. That's exactly what `Option<T>` is. It represents a value that might or might not exist. Why do we need this? Because in smart contracts, lots of things are optional. A user profile might not have a bio yet. A search might not find a match. Instead of using some magic value like `0` or `""` to mean "nothing" (which is confusing and error-prone), Move gives you `Option`: it's either `option::some(value)` (the box has something in it) or `option::none()` (the box is empty). Important: there is NO `null` in Move. If you're coming from JavaScript, Python, or Java, forget about null. Option is the Move way to say "this might not have a value."

📝

Strings: ascii and utf8

Before we dive in, let's talk about what "encoding" means. Computers store everything as numbers (bytes). Encoding is the rule book for turning text into numbers and back. ASCII is the simple rule book: each letter gets a number from 0 to 127 (A=65, B=66, etc.). It only covers English letters, digits, and basic symbols. UTF-8 is the big rule book: it covers every writing system on Earth — Chinese, Arabic, emoji, you name it. Move gives you three ways to handle text: 1. **`vector<u8>`** — Raw bytes. No validation. Created with `b"text"`. Like a plain envelope — you can put anything in it. 2. **`ascii::String`** — Validated ASCII only. Will crash if you try to put non-ASCII characters in it. Good for simple identifiers. 3. **`string::String`** — Validated UTF-8. Supports all languages and symbols. Best for user-facing text like display names. Both string types are just `vector<u8>` under the hood, but with a safety check when you create them.

Constants & Best Practices
📌

Declaring Constants

Constants are values you set in stone at the start and never change — like the speed limit on a highway. In Move, you declare them with the `const` keyword at the module level (outside of any function). They must have a type and a value that's known at compile time. By convention, regular constants use UPPER_SNAKE_CASE (like `MAX_SUPPLY`). But there's one really important pattern in Sui Move: error code constants. These start with a capital `E` (like `ENotAuthorized`). You'll use them with `assert!` to give meaningful names to the reasons your contract might reject a transaction. Instead of just seeing "error code 3," someone debugging can look up `EInvalidAmount` and immediately know what went wrong.

🧠

Type Inference & let Bindings

Good news: you don't always have to write out the type! Move is smart enough to figure it out from context. When you write `let x = 42`, the compiler looks at how you use `x` and determines the type. If nothing gives it a hint, number literals default to u64. You can always add an explicit type if you want to be clear: `let x: u8 = 42`. And there's one more thing — the `mut` keyword. By default, every variable in Move is immutable (can't be changed after creation). If you want to update a variable later, you need to say `let mut x = 5`. This immutability-by-default is another safety feature: it prevents you from accidentally changing something you didn't mean to.

🎯

Choosing the Right Type

Let's see — with all these types, how do you actually pick the right one? Here's a simple cheat sheet: - **Most numbers** (balances, amounts, counters, timestamps): Use **u64**. It's the standard in Sui. - **Small bounded values** (age, percentage, version number): Use **u8**. It signals "this value is small on purpose." - **Really big math** (DeFi calculations that might overflow u64): Use **u128** or **u256**. - **Text that users see** (names, descriptions): Use **string::String** (UTF-8). - **Internal identifiers** (tags, keys): Use **ascii::String** or **vector<u8>**. - **Something that might not exist** (optional bio, search result): Use **Option<T>**. - **Yes/no flags**: Use **bool**. - **Wallet addresses or object IDs**: Use **address**. When in doubt, go bigger on integers and use Option for anything that could be absent. It's better to be safe than to debug an overflow at 2 AM.