Types & Values

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Rust 1.99.0 and the 2024 edition, checked on 2026-10-06. Rust is statically typed: every value has a type known at compile time. This page covers the built-in primitive types — the scalars and the built-in compound types — plus the rules for converting between them. Slices and str have their own page (Slices & Dynamically Sized Types), as do user-defined types (Structs & Methods, Enums & Pattern Matching).

The primitive types at a glance

The table lists every primitive with its size on a typical 64-bit target, its range, and the type an unsuffixed literal gets when nothing else constrains it.

Type Size (bytes) Range / values Literal examples

i8 / u8

1

-128..=127 / 0..=255

-5i8, 255u8, b'A' (a u8)

i16 / u16

2

-32 768..=32 767 / 0..=65 535

1_000i16

i32 / u32

4

about ±2.1 × 109 / 0..=4.29 × 109

42 (default integer type)

i64 / u64

8

about ±9.2 × 1018 / 0..=1.8 × 1019

9_000_000_000i64

i128 / u128

16

about ±1.7 × 1038 / 0..=3.4 × 1038

1u128 << 100

isize / usize

pointer size (8 on 64-bit, 4 on 32-bit)

as i64/u64 on 64-bit targets

0usize; used for indexing and lengths

f32

4

IEEE 754 binary32, about 7 decimal digits

1.5f32

f64

8

IEEE 754 binary64, about 15—​16 decimal digits

1.5 (default float type)

bool

1

true, false

char

4

a Unicode scalar value: U+0000..=U+D7FF and U+E000..=U+10FFFF

'a', '\u{1F4DA}'

()

0

the single value ()

(T1, T2, ...)

sum of fields plus padding

a fixed-size heterogeneous group

(1, "Dune")

[T; N]

N × size of T

N values of one type, N known at compile time

[0u8; 16], [1, 2, 3]

!

0 (never has a value)

the type of expressions that never complete

panic!(), loop {}

You can check sizes yourself with std::mem::size_of (in the prelude since 1.80, so no import is needed):

fn main() {
    println!("u8 {}  u32 {}  u128 {}  usize {}", size_of::<u8>(), size_of::<u32>(), size_of::<u128>(), size_of::<usize>());
    println!("f64 {}  bool {}  char {}  () {}", size_of::<f64>(), size_of::<bool>(), size_of::<char>(), size_of::<()>());
    println!("(u8, u32) {}  [u16; 5] {}", size_of::<(u8, u32)>(), size_of::<[u16; 5]>());
    // u8 1  u32 4  u128 16  usize 8
    // f64 8  bool 1  char 4  () 0
    // (u8, u32) 8  [u16; 5] 10
}

The (u8, u32) tuple is 8 bytes, not 5, because the u32 must be 4-byte aligned; the compiler inserts padding (and may reorder fields). Layout is covered in Memory Layout & repr.

Integers

Integer types are named by signedness and width: i for signed (two’s complement), u for unsigned, then the number of bits. isize and usize have the width of a pointer on the target; usize is the type of indexes, lengths and sizes, so slicing and len() use it.

Literals can be written in decimal (98_222), hexadecimal (0xff), octal (0o77) or binary (0b1111_0000), with _ as a visual separator anywhere after the first digit, and with an optional type suffix (57u8, 1_000_i64). Without a suffix, the type is inferred from context; if nothing constrains it, it defaults to i32.

Which type to choose? Use i32/u32 (or i64/u64 when values can be large) for ordinary numbers, usize for anything that indexes memory, and the narrowest type that fits for data you store in bulk. Prefer unsigned types only when negative values are impossible and you want the compiler to say so; subtracting two usize values that can go below zero is a common source of overflow panics.

Integer overflow

What happens when arithmetic exceeds the range of the type depends on the build profile:

  • in debug builds (cargo build, cargo test), overflow panics: attempt to add with overflow;

  • in release builds (--release), overflow wraps around in two’s complement, silently.

This is controlled by the overflow-checks profile setting, so you can keep checks on in release builds if the performance cost is acceptable. Relying on either behaviour is a bug; when overflow is possible, say what you want with one of the explicit method families:

Family Behaviour on overflow 250u8 + 10

wrapping_*

wraps modulo 2N

wrapping_add → 4

checked_*

returns None

checked_add → None

saturating_*

clamps to the minimum or maximum

saturating_add → 255

overflowing_*

returns (wrapped, did_overflow)

overflowing_add → (4, true)

strict_* (1.91)

always panics, in every profile

strict_add → panic

std::num::Wrapping<T>

a wrapper whose operators all wrap

Wrapping(250u8) + Wrapping(10) → Wrapping(4)

use std::num::Wrapping;

fn main() {
    let copies: u8 = 250;
    assert_eq!(copies.wrapping_add(10), 4);
    assert_eq!(copies.checked_add(10), None);
    assert_eq!(copies.checked_add(5), Some(255));
    assert_eq!(copies.saturating_add(10), 255);
    assert_eq!(copies.overflowing_add(10), (4, true));
    assert_eq!((Wrapping(copies) + Wrapping(10)).0, 4);

    // Subtraction on unsigned values is the classic trap:
    let on_shelf: usize = 2;
    let requested: usize = 3;
    match on_shelf.checked_sub(requested) {
        Some(left) => println!("{left} left"),
        None => println!("not enough copies"),
    }
    // `on_shelf - requested` would panic in debug builds and wrap to 18446744073709551615 in release.

    let result = std::panic::catch_unwind(|| copies.strict_add(10));
    assert!(result.is_err()); // strict_* panics even in release builds
}

Integer division by zero always panics (attempt to divide by zero) — use checked_div when the divisor comes from input. Shifting by the type’s width or more is also an overflow (1u8 << 8). The integer types have many more methods: pow, abs, abs_diff, signum, rem_euclid, div_ceil, isqrt (1.84), count_ones, leading_zeros, rotate_left, to_be_bytes/from_le_bytes, ilog10, and MIN/MAX/BITS constants.

Converting strings to numbers

str::parse converts text to any type that implements FromStr, returning a Result because the text may be invalid. Specify the target type with an annotation or the turbofish:

fn main() {
    let year: u16 = "1965".parse().expect("a year");
    let pages = "412".parse::<u32>().unwrap_or(0);
    let bad = "12x".parse::<i32>();
    println!("{year} {pages} {bad:?}"); // 1965 412 Err(ParseIntError { kind: InvalidDigit })
    let hex = u8::from_str_radix("ff", 16);
    println!("{hex:?}"); // Ok(255)
}

Source: std: str::parse

Floating-point numbers

f32 and f64 are IEEE 754 binary floating-point numbers; f64 is the default because on modern CPUs it is about as fast as f32 and much more precise. Floats have the usual IEEE semantics: rounding error (0.1 + 0.2 != 0.3), signed zero, infinities, and NaN ("not a number"), produced by operations such as 0.0 / 0.0.

NaN is not equal to anything, including itself. That is why floats implement PartialEq and PartialOrd but not Eq, Ord or Hash: you cannot sort() a Vec<f64>, use floats as HashMap keys, or call max() on an iterator of floats. For a total order (NaN sorts after positive infinity, and -0.0 < +0.0), use total_cmp:

fn main() {
    let mut ratings = vec![4.5, f64::NAN, 3.0, 5.0, -0.0];
    // ratings.sort();  // does not compile: f64 is not Ord (E0277)
    ratings.sort_by(f64::total_cmp);
    println!("{ratings:?}"); // [-0.0, 3.0, 4.5, 5.0, NaN]

    let nan = f64::NAN;
    assert_eq!(nan.partial_cmp(&nan), None); // NaN is unordered, even with itself
    assert!(nan.is_nan());
    assert!((0.1 + 0.2 - 0.3_f64).abs() < f64::EPSILON); // compare with a tolerance
    let best = ratings.iter().copied().filter(|r| !r.is_nan()).fold(f64::MIN, f64::max);
    println!("{best}"); // 5
}

Float-to-integer casts with as saturate and map NaN to 0 (300.7_f64 as u8 == 255, f64::NAN as i32 == 0), a rule Rust defined in 1.45 so that the cast can never be undefined behaviour. For numerically careful code — NaN policies, mul_add, compensated summation, the algebraic float methods added in 1.98 — see Numbers & Numeric Computing.

Booleans

bool has the values true and false and is one byte in size. Conditions in if and while must be bool exactly; there is no truthiness, so if count {} with an integer is a type error (E0308). The logical operators && and || short-circuit, ! negates, and &, |, ^ also work on bool without short-circuiting. Two handy methods turn a condition into an Option: then_some(value) (eager) and then(|| value) (lazy):

fn main() {
    let in_stock = true;
    let label = in_stock.then_some("available");
    let count = 3;
    let note = (count > 2).then(|| format!("{count} copies"));
    println!("{label:?} {note:?}"); // Some("available") Some("3 copies")
    println!("{}", u8::from(in_stock)); // 1 -- bool converts losslessly to integers
}

Source: std: bool

Characters

char is a Unicode scalar value: any code point except the surrogates U+D800..=U+DFFF. It is always 4 bytes, whatever the character. Literals use single quotes: 'a', 'é', '\n', '\u{1F4DA}' (📚). A char is not a byte and not a "user-perceived character": 'e' followed by a combining accent is two chars, and Strings store UTF-8 bytes, not chars. See Strings & Text.

fn main() {
    let c = 'é';
    println!("{} {} {}", c.is_alphabetic(), c.len_utf8(), c as u32); // true 2 233
    println!("{:?}", 'a'.to_ascii_uppercase());                      // 'A'
    println!("{:?}", char::from_u32(0x1F4DA));                        // Some('📚')
    println!("{:?}", char::from_digit(7, 10));                        // Some('7')
    println!("{:?}", '9'.to_digit(10));                               // Some(9)
}

Tuples and the unit type

A tuple groups a fixed number of values of possibly different types. Access elements by position (t.0, t.1) or destructure them with a pattern. Tuples are handy for returning several values from a function; for anything with more than two or three fields, or that crosses an API boundary, a struct with named fields is clearer.

fn split_isbn(isbn: &str) -> (&str, &str) {
    isbn.split_at(3)
}

fn main() {
    let book: (&str, u16, f32) = ("Dune", 1965, 4.6);
    let (title, year, rating) = book; // destructuring
    println!("{title} ({year}) {rating} -- first field again: {}", book.0);

    let (prefix, rest) = split_isbn("9780441013593");
    println!("{prefix} / {rest}");

    let single = (5,);   // a one-element tuple needs the comma; (5) is just 5 in parentheses
    let unit = ();       // the empty tuple: the unit type and its only value
    println!("{single:?} {unit:?}");
}

The empty tuple () is the unit type. It has exactly one value, also written (), and occupies no memory. Functions without a return type return (), expressions ending in ; evaluate to (), and Result<(), E> means "success carries no data".

Arrays

An array [T; N] holds exactly N values of type T inline (on the stack, or inside whatever contains it). The length is part of the type: [u8; 4] and [u8; 5] are different types. Arrays are useful for fixed-size data (checksums, lookup tables, coordinates); for a collection that grows, use Vec<T>.

fn checksum(digits: [u8; 13]) -> u32 {
    digits.iter().enumerate().map(|(i, d)| u32::from(*d) * if i % 2 == 0 { 1 } else { 3 }).sum()
}

fn main() {
    let weekdays = ["Mon", "Tue", "Wed", "Thu", "Fri"];
    let zeros = [0u8; 16];                 // [value; length]
    println!("{} {} {}", weekdays[0], weekdays.len(), zeros.len());

    let isbn = [9, 7, 8, 0, 4, 4, 1, 0, 1, 3, 5, 9, 3];
    println!("valid: {}", checksum(isbn).is_multiple_of(10)); // is_multiple_of: 1.87

    let [first, .., last] = weekdays;      // array pattern
    println!("{first}..{last}");

    let doubled = [1, 2, 3].map(|n| n * 2); // arrays have `map`
    println!("{doubled:?}");
}

Indexing is bounds-checked: weekdays[7] with a constant index is a compile-time error, and with a runtime index it panics (index out of bounds: the len is 5 but the index is 7). Use get(i), which returns an Option, when the index comes from input. Arrays implement Copy and Clone if T does, IntoIterator by value (since the 2021 edition), Debug, PartialEq, Default for any length, and coerce to slices (&[T]), which is how they get most of their methods. The length can be a generic parameter — see Generics & Const Generics.

The never type

!, the never type, is the type of an expression that never produces a value: panic!(), loop {} without break, return, break, continue, and calls to functions declared -> ! such as std::process::exit. Because it has no values, ! coerces to any type, which is what lets one match arm return a value while another bails out:

fn parse_year(text: &str) -> u16 {
    match text.parse() {
        Ok(year) => year,          // u16
        Err(_) => panic!("bad year: {text}"), // `!` coerces to u16
    }
}

fn fail(code: i32) -> ! {
    eprintln!("fatal error");
    std::process::exit(code)
}

fn main() {
    println!("{}", parse_year("1965"));
    if std::env::args().count() > 5 {
        fail(2);
    }
}

Using ! as a type in other positions (for example Result<T, !>) is still unstable; on stable, the standard library uses the empty enum std::convert::Infallible for the same purpose.

Casting with as

as performs primitive conversions explicitly; Rust never converts numeric types implicitly, so let x: u64 = some_u32; is a type error (E0308). The rules:

  • integer to wider integer: zero-extends (unsigned source) or sign-extends (signed source);

  • integer to narrower integer: truncates the high bits (300u32 as u8 == 44);

  • signed to unsigned of the same width: reinterprets the bits (-1i8 as u8 == 255);

  • float to integer: rounds toward zero and saturates; NaN becomes 0;

  • integer to float: rounds to the nearest representable value;

  • u8 to char, char to any integer, bool to integer;

  • between raw pointer types, and between pointers and integers (see Unsafe Rust).

fn main() {
    let big: u32 = 300;
    println!("{}", big as u8);              // 44 -- silent truncation
    println!("{}", -1i8 as u8);             // 255
    println!("{}", 3.99_f64 as i32);        // 3
    println!("{}", -5.0_f64 as u8);         // 0 -- saturates
    println!("{}", 'A' as u32);             // 65
    println!("{}", 97u8 as char);           // a

    // Prefer lossless From and checked TryFrom for integer conversions:
    let wide: u64 = u64::from(big);
    let narrow = u8::try_from(big);
    println!("{wide} {narrow:?}");          // 300 Err(TryFromIntError(PosOverflow))
}

Because as silently truncates, prefer From (for conversions that cannot lose information) and TryFrom (for those that can) in new code; Clippy’s pedantic cast_possible_truncation and cast_sign_loss lints point out risky as casts. Conversions between your own types are the subject of Type Conversions & Coercions.

Type inference and type aliases

The compiler infers local types using the whole function body (a Hindley-Milner-style algorithm extended with trait resolution), so a variable’s type can be decided by a later use:

type Isbn13 = [u8; 13];              // an alias: just another name, not a new type
type Index = std::collections::HashMap<String, Vec<usize>>;

fn main() {
    let mut titles = Vec::new();     // Vec<?> for now...
    titles.extend(["Dune", "Emma"]); // ...now known to be Vec<&str>
    let mut index = Index::new();
    index.entry("dune".to_string()).or_default().push(0);
    let digits: Isbn13 = [9, 7, 8, 0, 4, 4, 1, 0, 1, 3, 5, 9, 3];
    println!("{titles:?} {index:?} {}", digits.len());
}

A type alias does not create a distinct type — Isbn13 and [u8; 13] are interchangeable — so it cannot enforce invariants. For that, use a newtype (a tuple struct around the value), as shelf-core does for Isbn; see Structs & Methods. Aliases are most useful for shortening long generic types, and the standard library uses them for module-specific results such as io::Result<T> (an alias for Result<T, io::Error>).

Operators

Rust’s operators are mostly familiar. Arithmetic (+ - * / %), comparison (== != < > <= >=), logical (&& || !), bitwise (& | ^ ! << >>), compound assignment (+=, <<=, …​), plus a few Rust-specific ones:

Operator Meaning

& / &mut

borrow (create a reference)

*

dereference

?

propagate an error or None (see Error Handling)

.., ..=

ranges: 0..10 (exclusive), 1..=100 (inclusive), .., 5.., ..3

as

primitive cast

::

path separator (std::io::stdin, Vec::<u8>::new)

->

function return type

=>

match arm

! after a name

macro invocation (vec![], println!())

There is no +`/`--`, no ternary `? :` (use `if` as an expression), and `^+ is XOR, not exponentiation (use pow). Arithmetic operands must have the same type — 1u8 + 1u32 is error E0308 — and operators are defined by the traits in std::ops, so your own types can overload them. The complete table is in The Book’s Appendix B.

Common errors

Error Typical cause

E0308 mismatched types

mixing integer widths (u8 + u32), using an integer as a condition, returning the wrong type

E0277 trait bound not satisfied

sort() on Vec<f64>, a float as a HashMap key, u32 + f64

E0282 type annotations needed

parse() or collect() without a target type

literal out of range (deny lint)

let x: u8 = 256;

this arithmetic operation will overflow (deny lint)

overflow the compiler can see at compile time, such as 255u8 + 1

Where the books are outdated

The books consulted for this section predate several numeric additions: the strict_* arithmetic methods (1.91), isqrt (1.84), size_of and align_of in the prelude (1.80), and the algebraic float methods (1.98). Numerical material in the Crossley et al. book was used only to check topic coverage; the float semantics described here come from the Reference and the standard library documentation.