Skip to main content
Rust beginner Lesson 4 of 30

Data Types in Rust

Explore Rust's scalar types, compound types (tuples and arrays), and type aliases.

Scalar Types

Scalar types represent a single value. Rust has four primary scalar kinds: integers, floating-point numbers, booleans, and characters. Every value in a Rust program has a type, and the compiler uses these types to catch misuse at compile time — there is no runtime type coercion or implicit conversion between scalar types.

Integers

Rust gives you precise control over integer size and signedness, which matters when you are targeting embedded hardware, writing network protocols, or working with binary formats. The default integer type — i32 — is the right choice when you do not have a specific reason to pick something else.

LengthSignedUnsigned
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
archisizeusize

isize/usize match the pointer width of the target platform (64-bit on modern systems). They are the natural type for indexing slices and collections because array indices must match the platform’s address size.

fn main() {
    let a: i32  = -2_147_483_648; // minimum i32 value
    let b: u8   = 255;            // maximum u8 value
    let c: i64  = 9_223_372_036_854_775_807; // maximum i64
    let index: usize = 0;         // always use usize for array/slice indices

    // Integer overflow is a panic in debug mode — the compiler won't silently corrupt data
    // let overflow: u8 = 256; // ERROR: literal out of range
}

Rust panics on integer overflow in debug builds and wraps silently in release builds. When you need predictable wrap/clamp behaviour, use the explicit methods:

fn main() {
    let x: u8 = 250;
    println!("{}", x.wrapping_add(10));    // 4   — wraps around past 255
    println!("{}", x.saturating_add(10));  // 255 — clamps to the maximum
    println!("{:?}", x.checked_add(10));   // None — signals overflow via Option
    println!("{:?}", x.overflowing_add(10)); // (4, true) — value and overflow flag
}

Floating-Point Numbers

Rust has two floating-point types following IEEE 754. f64 is the default because on modern 64-bit CPUs it is the same speed as f32 while offering significantly more precision. Reach for f32 only when you have a specific reason — GPU computation, tight memory budgets, or interoperating with C APIs that use float.

fn main() {
    let f32_val: f32 = 3.14;
    let f64_val: f64 = 3.141_592_653_589_793; // default floating-point type

    // Standard math operations are methods on the type
    let sqrt  = f64_val.sqrt();
    let abs   = (-2.5_f64).abs();
    let ceil  = 2.1_f64.ceil();    // 3.0 — round up
    let floor = 2.9_f64.floor();   // 2.0 — round down
    let pow   = 2.0_f64.powi(10);  // 1024.0 — integer exponent

    println!("{:.4}", sqrt); // 1.7725
    println!("{}", pow);     // 1024
}

Booleans

The bool type has exactly two values: true and false. Booleans are the result of comparison and logical expressions, and they are the condition type for if and while. Unlike C, Rust does not allow integers to stand in for booleans — the types are kept strictly separate.

fn main() {
    let t: bool = true;
    let f: bool = false;

    println!("{}", t && f);  // false — logical AND
    println!("{}", t || f);  // true  — logical OR
    println!("{}", !t);      // false — logical NOT

    // Booleans are exactly 1 byte in memory
    println!("{}", std::mem::size_of::<bool>()); // 1
}

Characters

char represents a single Unicode scalar value, not a byte. This distinction matters: a char is always 4 bytes, can hold any Unicode code point, and is the correct type for working with text character by character. String iterating and indexing use char rather than bytes.

fn main() {
    let letter: char = 'A';
    let emoji: char  = '😊';  // emoji are valid chars
    let cjk: char    = '中';   // Chinese/Japanese/Korean characters too

    println!("{} {} {}", letter, emoji, cjk);
    println!("{}", std::mem::size_of::<char>()); // 4 bytes always
    println!("{}", letter.is_alphabetic());       // true
    println!("{}", '9'.is_numeric());             // true
    println!("{}", letter.to_lowercase().next().unwrap()); // a
}

Compound Types

Compound types group multiple values into one type. Rust’s two built-in compound types — tuples and arrays — differ in one key way: tuples can hold different types, arrays hold a single type.

Tuples

Tuples group a fixed number of values that can be of different types. They are useful for returning multiple values from a function without defining a dedicated struct, and for quick ad-hoc groupings. Elements are accessed by zero-based index with dot notation, or by destructuring.

fn main() {
    let point: (f64, f64) = (3.0, -1.5);
    let rgb: (u8, u8, u8) = (255, 128, 0);
    let mixed: (i32, bool, &str) = (42, true, "hello");

    // Access individual fields by index
    println!("x={}, y={}", point.0, point.1);

    // Destructure into named bindings — often cleaner than indexed access
    let (r, g, b) = rgb;
    println!("r={}, g={}, b={}", r, g, b);

    // The unit tuple () — zero elements, used as the "no return value" type
    let unit: () = ();
}

Tuples are particularly convenient for returning multiple values from a function without the overhead of defining a struct:

fn min_max(slice: &[i32]) -> (i32, i32) {
    let mut min = slice[0];
    let mut max = slice[0];
    for &val in &slice[1..] {
        if val < min { min = val; }
        if val > max { max = val; }
    }
    (min, max) // return both values in a tuple
}

fn main() {
    let data = [3, 1, 4, 1, 5, 9, 2, 6];
    let (lo, hi) = min_max(&data); // destructure the result immediately
    println!("min={}, max={}", lo, hi); // min=1, max=9
}

Arrays

Arrays have a fixed size known at compile time and are stored entirely on the stack. This makes them very fast — no heap allocation, no indirection — but it means the size must be a constant. When you need a dynamically-sized collection, use Vec<T> instead.

fn main() {
    // Type annotation syntax: [ElementType; Length]
    let primes: [u32; 5] = [2, 3, 5, 7, 11];

    // Initialise all elements to the same value
    let zeros = [0u8; 256]; // 256 bytes, all zero — common for buffers

    // Access by index — bounds are checked at runtime (panics, not UB)
    println!("{}", primes[0]); // 2
    println!("{}", primes.len()); // 5

    // Iterate over borrowed elements
    for p in &primes {
        print!("{} ", p);
    }
    println!();

    // Slices — a borrowed view into a contiguous sequence
    let middle: &[u32] = &primes[1..4]; // [3, 5, 7] — no copy
    println!("{:?}", middle);
}

Out-of-bounds access panics at runtime (not undefined behaviour like in C). For safe access when the index might be out of range, use .get() which returns an Option:

fn main() {
    let a = [1, 2, 3];
    // let x = a[5]; // panics: index out of bounds at runtime

    // Safe alternative — returns None instead of panicking
    if let Some(x) = a.get(5) {
        println!("{}", x);
    } else {
        println!("index out of range"); // prints this
    }
}

Type Aliases

type creates an alias — a new name for an existing type. It does not create a distinct type; the two names are fully interchangeable. Aliases improve readability by making intent clear, especially in function signatures where raw f64 parameters do not tell the reader what the value represents.

type Meters = f64;
type Seconds = f64;
type Velocity = f64;

// The signature now reads like documentation
fn speed(distance: Meters, time: Seconds) -> Velocity {
    distance / time
}

fn main() {
    let d: Meters = 100.0;
    let t: Seconds = 9.58;
    println!("{:.2} m/s", speed(d, t)); // 10.44 m/s
}

Type aliases improve readability and document intent. For stricter type safety — preventing accidental mixing of Meters and Seconds at the type level — use the newtype pattern (a tuple struct with one field) instead. Aliases are transparent to the compiler; newtypes are distinct types.

The str and String Types

Rust has two string types that serve different purposes. Understanding when to use each one is important because they appear constantly in real code.

fn main() {
    // &str — a string slice; a borrowed reference to UTF-8 encoded string data
    // String literals are &str and live in the compiled binary
    let s1: &str = "hello, world";

    // String — heap-allocated, growable, owned UTF-8 string
    // Use String when you need to build or modify string content at runtime
    let s2: String = String::from("hello");
    let s3 = "world".to_string(); // convert &str to String
    let s4 = format!("{}, {}!", s2, s3); // format! always returns a String

    println!("{}", s4);           // hello, world!
    println!("length: {}", s4.len()); // length in bytes, not Unicode characters
}

Type Casting with as

Rust does not implicitly coerce numeric types. Every conversion must be explicit. The as keyword performs primitive casts, truncating or wrapping as needed. Because as can silently lose data, prefer the safer From/Into or TryFrom/TryInto traits for non-trivial conversions.

fn main() {
    let x: i32 = 1000;
    let y = x as u8;   // truncates: 1000 % 256 = 232 (data loss, but intentional)
    let z = x as f64;  // widens losslessly: 1000.0

    println!("{} {} {}", x, y, z);

    let f: f64 = 3.99;
    let i = f as i32;  // truncates toward zero: 3 (fractional part discarded)
    println!("{}", i);
}

For safe, checked conversions that return an error instead of truncating:

fn main() {
    let n: i64 = 1000;
    let m = i32::try_from(n).unwrap_or(i32::MAX); // returns Err if value doesn't fit
    println!("{}", m); // 1000

    let too_big: i64 = 3_000_000_000;
    let clamped = i32::try_from(too_big).unwrap_or(i32::MAX);
    println!("{}", clamped); // 2147483647 — safely handled
}

Frequently Asked Questions

What is the default integer type in Rust?
i32. It is generally the fastest integer type on modern hardware and is the type inferred when you write a plain integer literal.
What is the difference between an array and a Vec in Rust?
Arrays have a fixed size known at compile time and live on the stack. Vec is a heap-allocated, dynamically-sized collection.
Can Rust tuples hold different types?
Yes. Tuples are heterogeneous — each element can be a different type.