RustData Types

Data Types in Rust

Every value in Rust has a type, and the compiler must know that type at compile time. Rust is a statically-typed language. Most of the time the compiler infers the type for you; occasionally you must provide an explicit annotation. Understanding Rust's type system is fundamental to writing correct, efficient code.

Rust's types fall into two broad categories: scalar types (a single value) and compound types (multiple values grouped together).

Scalar Types

A scalar type represents a single value. Rust has four primary scalar types: integers, floating-point numbers, booleans, and characters.

Integer Types

An integer is a whole number — no decimal point. Rust provides signed integers (which can be negative) and unsigned integers (always non-negative). The number in the type name is the bit width.

Type

Signed?

Size

Min value

Max value

i8

Yes

8-bit

-128

127

i16

Yes

16-bit

-32,768

32,767

i32

Yes

32-bit

-2,147,483,648

2,147,483,647

i64

Yes

64-bit

-9.2 × 10¹⁸

9.2 × 10¹⁸

i128

Yes

128-bit

-1.7 × 10³⁸

1.7 × 10³⁸

isize

Yes

pointer-sized

platform-dependent

platform-dependent

u8

No

8-bit

0

255

u16

No

16-bit

0

65,535

u32

No

32-bit

0

4,294,967,295

u64

No

64-bit

0

1.8 × 10¹⁹

u128

No

128-bit

0

3.4 × 10³⁸

usize

No

pointer-sized

0

platform-dependent

Note
The default integer type is i32 — generally the fastest even on 64-bit systems. Use usize and isize for indexing collections and pointer arithmetic.

RUST
fn main() {
    let a: i8  = -100;
    let b: u8  = 200;
    let c: i32 = -2_147_483_648;    // i32 minimum
    let d: u32 = 4_294_967_295;     // u32 maximum
    let e: i64 = 9_223_372_036_854_775_807;
    let f: usize = 42;              // typically used for indexing

    println!("i8={} u8={} i32={} u32={} i64={} usize={}", a, b, c, d, e, f);
}
i8=-100 u8=200 i32=-2147483648 u32=4294967295 i64=9223372036854775807 usize=42
Integer Overflow

Integer overflow occurs when a calculation produces a value outside the type's range. Rust handles this differently depending on the build profile.

Warning
In **debug** builds, integer overflow causes a runtime panic — Rust intentionally crashes your program rather than silently producing a wrong answer. In **release** builds (--release), overflow wraps around using two's complement arithmetic (e.g. 255u8 + 1 becomes 0). Never rely on wrapping behaviour in release mode unless you use the explicit wrapping methods.

RUST
fn main() {
    let max: u8 = 255;

    // This panics in debug mode:
    // let overflow = max + 1;

    // Use explicit methods to control overflow behaviour:
    let wrapped   = max.wrapping_add(1);    // 0  (wraps around)
    let checked   = max.checked_add(1);     // None (returns Option)
    let saturated = max.saturating_add(1);  // 255 (clamps to max)
    let (val, overflowed) = max.overflowing_add(1); // (0, true)

    println!("wrapping:   {}", wrapped);
    println!("checked:    {:?}", checked);
    println!("saturated:  {}", saturated);
    println!("overflowing: val={} overflowed={}", val, overflowed);
}
wrapping:   0
checked:    None
saturated:  255
overflowing: val=0 overflowed=true
Floating-Point Types

Rust has two floating-point types: f32 (32-bit, single precision) and f64 (64-bit, double precision). The default is f64 — it is the same speed as f32 on modern hardware and offers more precision.

RUST
fn main() {
    let x = 2.0;          // f64 by default
    let y: f32 = 3.0;     // explicit f32

    let sum  = x + 2.5;
    let diff = x - 1.1;
    let prod = x * y as f64;
    let quot = x / 0.7;
    let rem  = x % 1.5;

    println!("sum={:.2} diff={:.2} prod={:.2} quot={:.2} rem={:.2}",
             sum, diff, prod, quot, rem);
}
sum=4.50 diff=0.90 prod=6.00 quot=2.86 rem=0.50
Boolean Type

The bool type has exactly two values: true and false. Booleans are one byte in size and are the result of comparison and logical operations.

RUST
fn main() {
    let t = true;
    let f: bool = false;  // explicit annotation

    // Logical operators
    println!("AND: {}", t && f);   // false
    println!("OR:  {}", t || f);   // true
    println!("NOT: {}", !t);       // false

    // Comparisons produce bools
    let x = 5;
    let is_even = x % 2 == 0;
    let is_big  = x > 100;
    println!("is_even={} is_big={}", is_even, is_big);
}
AND: false
OR:  true
NOT: false
is_even=false is_big=false
The char Type

Rust's char type represents a single Unicode Scalar Value. It is specified with single quotes (strings use double quotes). Crucially, char is four bytes in size — large enough to hold any Unicode character, including emoji and characters from non-Latin scripts.

RUST
fn main() {
    let letter: char = 'A';
    let heart:  char = '❤';  // ❤  via Unicode escape
    let emoji:  char = '🦀';        // Rust's mascot, Ferris

    println!("{} {} {}", letter, heart, emoji);
    println!("Size of char: {} bytes", std::mem::size_of::<char>());
    println!("Is alphabetic: {}", letter.is_alphabetic());
    println!("Is numeric:    {}", '9'.is_numeric());
    println!("To uppercase:  {}", 'a'.to_uppercase().next().unwrap());
}
A ❤ 🦀
Size of char: 4 bytes
Is alphabetic: true
Is numeric:    true
To uppercase:  A
Note
Do not confuse char (4 bytes, Unicode Scalar Value) with a single byte. String slices (&str) are UTF-8 encoded, so indexing by byte offset is not the same as indexing by character.
Compound Types — Tuples

A tuple groups together values of different types into a single compound value. Tuples have a fixed length — once declared, they cannot grow or shrink. Access individual elements with dot notation and a zero-based index.

RUST
fn main() {
    let person: (&str, u8, f64) = ("Alice", 28, 1.72);

    // Access by index
    let name   = person.0;
    let age    = person.1;
    let height = person.2;
    println!("{} is {} years old, {:.2}m tall", name, age, height);

    // Destructure into separate bindings
    let (city, population, area) = ("Berlin", 3_645_000_u32, 891.8_f64);
    println!("{}: {} people, {:.1} km²", city, population, area);

    // Unit tuple — the empty tuple () — is Rust's "nothing" type
    let unit: () = ();
    println!("Unit value: {:?}", unit);
}
Alice is 28 years old, 1.72m tall
Berlin: 3645000 people, 891.8 km²
Unit value: ()
Compound Types — Arrays

An array groups together values of the same type into a fixed-length sequence stored contiguously on the stack. Unlike vectors, arrays cannot grow or shrink at runtime. Arrays are ideal when you know the exact number of elements at compile time.

RUST
fn main() {
    // Declaration: [type; length]
    let primes: [u32; 5] = [2, 3, 5, 7, 11];

    // Access by index (zero-based)
    println!("First prime: {}", primes[0]);
    println!("Last prime:  {}", primes[4]);

    // Array length
    println!("Count: {}", primes.len());

    // Initialise all elements to the same value: [value; length]
    let zeros = [0u8; 8];
    println!("zeros: {:?}", zeros);

    // Iterating
    for prime in &primes {
        print!("{} ", prime);
    }
    println!();
}
First prime: 2
Last prime:  11
Count: 5
zeros: [0, 0, 0, 0, 0, 0, 0, 0]
2 3 5 7 11
Warning
Accessing an array out of bounds (e.g. primes[10]) causes a runtime panic in Rust, not undefined behaviour. Rust checks bounds on every array access unless the compiler can prove at compile time that the index is valid.
Type Inference vs Explicit Annotations

Rust's type inference is powerful enough to handle most everyday code. You only need explicit annotations when:

  • The compiler says it cannot infer the type (e.g. parsing a string into a number)

  • You want a type different from the default (e.g. u8 instead of the default i32)

  • Clarity for the reader outweighs brevity (documenting the expected range)

  • You are writing a public API where the types should be obvious from the signature

RUST
fn main() {
    // Inferred — works fine
    let x = 42;
    let y = 3.14;
    let flag = true;

    // Must be explicit — the compiler cannot guess which integer type to parse into
    let parsed: i32 = "100".parse().unwrap();

    // Want u8, not the default i32
    let byte: u8 = 255;

    println!("x={} y={} flag={} parsed={} byte={}", x, y, flag, parsed, byte);
}
x=42 y=3.14 flag=true parsed=100 byte=255
Type Casting with as

The as keyword performs explicit type conversion between numeric types. It is not the same as a safe conversion — it can truncate or reinterpret bits, so use it carefully.

RUST
fn main() {
    let x: i32 = 300;
    let y = x as u8;    // 300 truncates to 44 (300 - 256)
    println!("300 as u8 = {}", y);

    let big: i32 = -1;
    let unsigned = big as u32;  // reinterprets as 4294967295
    println!("-1 as u32 = {}", unsigned);

    let f = 3.99_f64;
    let i = f as i32;   // truncates towards zero, not rounds
    println!("3.99 as i32 = {}", i);  // 3

    let neg = -3.99_f64;
    let neg_i = neg as i32;
    println!("-3.99 as i32 = {}", neg_i);  // -3
}
300 as u8 = 44
-1 as u32 = 4294967295
3.99 as i32 = 3
-3.99 as i32 = -3
Warning
as truncates floats towards zero — it does not round. Use.round() as i32 if you need rounding behaviour. It also silently truncates integers when casting to a narrower type.
usize and isize

usize and isize are pointer-sized integer types — 32 bits on a 32-bit platform and 64 bits on a 64-bit platform. They are used wherever the size or index of a memory region is needed.

  • usize — use for array indices, slice lengths, and loop counters over collections

  • isize — use for pointer arithmetic or when a difference between two pointers could be negative

  • Rust requires array and slice indices to be usize — the compiler will tell you if you pass an i32

  • On most modern hardware both are 64 bits, but portable code should not assume this

RUST
fn main() {
    let items = [10, 20, 30, 40, 50];
    let index: usize = 2;          // must be usize for indexing
    println!("items[{}] = {}", index, items[index]);

    let len: usize = items.len();  // len() returns usize
    println!("length: {}", len);

    // Pointer size
    println!("usize is {} bytes on this platform",
             std::mem::size_of::<usize>());
}
items[2] = 30
length: 5
usize is 8 bytes on this platform
Success
Understanding Rust's type system is the foundation for everything else. Scalars give you the building blocks; compounds let you group them. The compiler enforces correct usage at compile time, so type errors are caught early — never at runtime.