Data Types in Rust
A comprehensive overview of Rusts type system covering scalar types, compound types, pointer types, arrays, vectors, slices, and string types with beginner-friendly explanations
Every value in a Rust program has a type. Before your code ever runs, the Rust compiler examines every variable, every function argument, and every expression to verify that types are used consistently throughout your program. This process catches entire categories of bugs at compile time — bugs that in dynamically typed languages would only surface at runtime, often in production.
Rust's type system is static, meaning every type must be known at compile time. The compiler can infer types in most situations, so you do not need to annotate everything. But when inference is ambiguous or you want to make your intent explicit, you write the type yourself. This balance — inference for convenience, explicit annotation for clarity — is central to how Rust programmers work with types day to day.
This page introduces every major category of data types in Rust. Each section links to a dedicated page with deeper explanations, more examples, and common pitfalls.
How to Use This Section:
If you are new to Rust, read the Scalar Types and Compound Types sections first. Those are the types you will use in nearly every line of Rust you write. The String Types section is essential but trickier — come back to it after you have written a few small programs. Pointer types and the distinction between arrays, vectors, and slices become important once you start managing collections of data.
Scalar Types
A scalar type holds a single value. Rust has four scalar types: integers, floating-point numbers, Booleans, and characters. These are the atoms of Rust's type system — everything else is built from them or builds upon them.
Integers are whole numbers. Rust gives you explicit control over both the size (8, 16, 32, 64, or 128 bits) and the signedness (signed with i, unsigned with u). The default integer type is i32. Floating-point numbers use f32 or f64, with f64 as the default. Booleans are bool with values true and false. Characters are char, representing a single Unicode scalar value in 4 bytes.
let temperature: i32 = -15; // signed 32-bit integer
let population: u64 = 8_000_000; // unsigned 64-bit, underscores for readability
let price: f64 = 29.99; // 64-bit float (default)
let is_active: bool = true; // boolean
let letter: char = 'R'; // unicode character
let crab: char = '🦀'; // char holds any unicode scalar value
When you run this code, each variable is allocated exactly the memory its type requires — an i32 takes 4 bytes, an f64 takes 8 bytes, a bool takes 1 byte, and a char takes 4 bytes. The compiler enforces that you cannot accidentally stuff a u64 into an i32 slot without an explicit conversion. What looks like a small constraint catches an enormous number of real-world bugs: buffer overflows, precision loss, sign errors in financial calculations.
A mental model that helps beginners: think of Rust's integer types like labeled containers of fixed sizes. A small container (i8) cannot hold a large value, and a signed container (i32) cannot hold an unsigned one. The compiler refuses to let you pour one container into another without saying exactly how — this is type safety in action.
Integer Overflow Panics in Debug Mode:
In debug builds, Rust checks for integer overflow at runtime and panics if it occurs. If you assign a value that exceeds the type's range — for example, let x: u8 = 256; — the compiler catches it. If overflow happens through arithmetic at runtime (like 255u8 + 1), the program panics in debug mode. In release mode, it wraps around using two's complement. This is a deliberate design choice: catch the bug during development, accept the performance trade-off in production.
Floating-Point Numbers Are Approximations:
Both f32 and f64 follow the IEEE 754 standard. They cannot represent every decimal number exactly — 0.1 + 0.2 does not equal 0.3 in floating-point arithmetic. Do not use floats for money, identity comparisons, or anything where exact decimal representation matters. Use the rust_decimal crate or integers (storing cents as a u64) for financial calculations.
For the complete reference on integer ranges, floating-point precision, and the char type's relationship to Unicode, see the Scalar Types page.
Compound Types
Compound types group multiple values into a single entity. Rust has two primitive compound types: tuples and arrays. Both have fixed lengths — once created, they do not grow or shrink.
Tuples group values of different types. They are the go-to tool when a function needs to return more than one value, or when you want to pass a small, fixed set of related data around without defining a struct. You access tuple elements by position using dot notation: tuple.0, tuple.1, and so on.
Arrays group multiple values of the same type. Their length is part of their type — [i32; 3] and [i32; 5] are different types. Arrays live on the stack, making them fast to create and access. Use them when you know the number of elements at compile time and that number will not change.
// A tuple holding three different types
let person: (&str, i32, bool) = ("Ada", 28, true);
println!("Name: {}, Age: {}", person.0, person.1);
// Destructuring a tuple into named variables
let (name, age, active) = person;
println!("{} is {} years old", name, age);
// An array of exactly five 32-bit integers
let scores: [i32; 5] = [85, 92, 78, 95, 88];
println!("First score: {}", scores[0]);
The tuple example creates a single value that bundles a string slice, an integer, and a boolean — three pieces of data that travel together. Destructuring unpacks them into separate variables in one line, which is cleaner than accessing each by index. The array example shows the syntax: type first, then length in brackets. Rust checks array bounds at runtime and panics immediately if you try to access scores[10] — there is no undefined behavior, no silent memory corruption.
Beginners sometimes reach for tuples when a struct with named fields would be clearer. If your tuple has more than three elements, or the meaning of each position is not obvious from context, define a struct instead. person.0 tells you nothing about what that field represents. person.name is unambiguous.
Destructuring Makes Tuples Readable:
If you use a tuple and immediately destructure it into named variables — as shown with let (name, age, active) = person; — you get the convenience of grouping without sacrificing readability. This pattern is common in Rust codebases and signals that the grouped values are only temporarily associated.
For destructuring patterns, the unit type (), and how tuples interact with functions, see the Compound Types page.
Pointer Types
A pointer is a value that stores the memory address of another value. Rust's pointer types are the mechanism behind its ownership and borrowing system — they are why Rust can guarantee memory safety without a garbage collector.
The pointer type you will use most often is the reference, written as &T for an immutable reference and &mut T for a mutable one. References are the foundation of borrowing: they let one part of your program access data owned by another part without taking ownership. The compiler enforces strict rules — you can have either one mutable reference or any number of immutable references to a value, but never both simultaneously.
Rust also has raw pointers (*const T and *mut T), which are unchecked and used primarily in unsafe code and FFI boundaries. Smart pointers — Box<T>, Rc<T>, Arc<T>, RefCell<T> — extend the pointer concept with additional metadata and behavior. They are covered in depth in the Smart Pointers chapter.
let x: i32 = 42;
let ref_to_x: &i32 = &x; // immutable reference to x
let another_ref: &i32 = &x; // multiple immutable references are fine
let mut y: i32 = 10;
let ref_to_y: &mut i32 = &mut y; // mutable reference
*ref_to_y += 5; // dereference to modify y
Here, ref_to_x does not own the number 42 — x does. The reference simply points to it. The *ref_to_y syntax dereferences the mutable reference, reaching through it to modify the underlying value. After this code runs, y is 15. The key insight for beginners: references are like arrows pointing at existing data. They do not create copies or take ownership.
The most common beginner mistake with references is fighting the borrow checker — trying to take a mutable reference while immutable references still exist, or trying to use a value after it has been moved. These errors feel frustrating at first, but they are the compiler preventing use-after-free bugs, data races, and iterator invalidation at compile time.
For the mechanics of borrowing, dereferencing, and the relationship between references and ownership, see the Pointer Types page.
Arrays, Vectors, and Slices
Scalar types hold one value. Compound types hold a few values of known count. But real programs need to work with collections of data where the size is not known until runtime — user input, file contents, database rows. Rust provides three tools for this: arrays, vectors, and slices.
Arrays ([T; N]) are fixed-size, stack-allocated sequences where N is known at compile time. Vectors (Vec<T>) are growable, heap-allocated sequences that can expand as needed. Slices (&[T]) are views into a contiguous sequence — they can point to an entire array, a portion of a vector, or any contiguous range of elements.
// Array: size fixed at compile time, lives on the stack
let fixed: [i32; 3] = [10, 20, 30];
// Vector: growable, lives on the heap
let mut growable: Vec<i32> = vec![10, 20, 30];
growable.push(40);
// Slice: a view into the vector
let view: &[i32] = &growable[1..3]; // elements at indices 1 and 2
println!("{:?}", view); // prints [20, 30]
The array fixed takes 12 bytes on the stack (3 × 4 bytes for i32) and cannot change size. The vector growable allocates its data on the heap and stores a pointer, length, and capacity on the stack — it can grow with .push(). The slice view is a fat pointer: it stores a pointer to the starting element and a length. Slices do not own the data they point to; they borrow it.
A practical way to decide: use an array when the count is a constant (days of the week, chess board squares). Use a Vec when the count varies (lines in a file, search results). Use a slice as a function parameter when you want to accept any contiguous sequence regardless of whether it comes from an array, a vector, or another slice.
Array Indexing Panics on Out-of-Bounds Access:
Accessing fixed[5] on a 3-element array compiles fine but panics at runtime with an index-out-of-bounds error. Rust does not silently read garbage memory — it immediately terminates the thread. For fallible indexing, use the .get() method, which returns an Option<&T>: fixed.get(5) returns None instead of panicking.
For vector capacity management, slice patterns, and the performance characteristics of each collection type, see the Arrays, Vectors, and Slices page.
String Types
Rust has two string types, and understanding the difference between them is one of the first real challenges every Rust learner faces. The two types are String and &str (pronounced "string slice").
A String is an owned, heap-allocated, growable sequence of UTF-8 bytes. When you create a String, you own the memory it occupies. When the String goes out of scope, that memory is freed. You can push characters onto it, truncate it, and modify it freely.
A &str is a borrowed reference to a sequence of UTF-8 bytes stored somewhere else — in static memory (for string literals), on the heap (as a slice of a String), or on the stack. It is immutable and does not own the data it points to.
// A string literal: type is &str, stored in the binary's static memory
let greeting: &str = "Hello, world!";
// An owned String: allocated on the heap, can be modified
let mut name: String = String::from("Alice");
name.push_str(" Smith");
// A slice of the String: borrows part of the owned data
let first_name: &str = &name[0..5];
println!("{}", first_name); // prints "Alice"
String literals like "Hello, world!" have the type &str and are baked into the compiled binary. They exist for the entire duration of the program. A String, by contrast, is created at runtime from user input, file reads, or by converting a literal with .to_string() or String::from(). The slice &name[0..5] creates a &str that borrows a portion of the String — no copying occurs.
A mental model: think of String as a filled-out form that you own and can edit. Think of &str as a photocopy of the form or a portion of it — you can read it, but you cannot change the original through it, and you must return it when you are done.
String Indexing Is Not Byte-Level:
You cannot index into a String with name[0] to get the first character. Rust strings are UTF-8 encoded, and a single character can span 1 to 4 bytes. Indexing by byte position could land in the middle of a multi-byte character, producing invalid UTF-8. Use .chars() for character iteration or slice by byte range only when you know the byte boundaries.
Accept &str in Function Parameters:
When writing a function that reads string data without needing to own or modify it, accept &str as the parameter type. Callers can pass a &String (which automatically coerces to &str), a string literal, or a slice — your function works with all of them. This is an idiomatic Rust pattern you will see in virtually every codebase.
For UTF-8 encoding details, string capacity management, and the full set of string manipulation methods, see the String Types page.
Beyond the Basics
The types covered on this page — scalars, compounds, pointers, arrays, vectors, slices, and strings — are the foundation. You will use them in every Rust program you write. But Rust's type system extends far beyond these primitives.
Structs let you define your own named composite types with fields. Enums let you define types that represent one of several variants, each optionally carrying data — this is the mechanism behind Option<T> and Result<T, E>, two types that replace null and exceptions in Rust. Traits define shared behavior across types, enabling polymorphism without inheritance. Generics let you write functions and types that operate on many concrete types while remaining type-safe.
The chapters that follow build on the concepts introduced here. When you encounter a function signature like fn parse<F: FromStr>(&self) -> Result<F, F::Err>, every piece of that signature — the reference, the generic type parameter, the trait bound, the Result enum — traces back to the type fundamentals covered on this page.
Type Annotations Are Documentation:
Even when the compiler can infer a type, annotating it explicitly can make your code more readable for other developers — and for your future self. A type annotation communicates intent. When a function's purpose is not obvious from its name alone, seeing fn calculate(price: f64, quantity: u32) -> f64 tells you more than fn calculate(price, quantity) ever could.
The single most important habit to build early: pay attention to compiler error messages about types. Rust's error messages are unusually detailed and often suggest the exact fix. Reading them carefully teaches you the type system faster than any tutorial.
Summary
Every type system makes a trade-off between expressiveness and safety. Rust chooses safety without sacrificing expressiveness — it gives you fine-grained control over memory layout (choose i8 or i64, choose stack or heap) while ensuring that you cannot accidentally misuse the choices you made. The compiler verifies every type interaction before the program runs.
What ties the sections on this page together is a single principle: in Rust, types describe not just what a value is, but how it behaves — who owns it, who can modify it, how long it lives, and how much space it occupies. A String and a &str differ not just in syntax but in ownership semantics. An array and a vector differ not just in mutability but in where their data lives. Understanding these distinctions is what moves you from writing Rust that merely compiles to writing Rust that is idiomatic, efficient, and clear.