Polyglot
Languages Rust types
Rust § types

Types

Rust’s type system is statically checked, with type inference that admits substantial omission of explicit annotations. The principal types are primitives (integers, floats, bool, char, ()), compound types (tuples, arrays, slices), references (&T and &mut T), user-defined types (struct, enum), and trait objects (dyn Trait). The type system is the foundation on which ownership, borrowing, and lifetimes rest; reading non-trivial Rust code requires fluency with the type-system surface and the conventional shapes (Vec<T>, String, Option<T>, Result<T, E>, Box<T>, HashMap<K, V>).

This page covers the principal types; the dedicated pages cover Ownership and borrowing, Traits, Generics, and Smart pointers.

Primitive types

The principal primitives:

TypeDescription
i8, i16, i32, i64, i128, isizeSigned integers (8 to 128 bits, plus pointer-sized)
u8, u16, u32, u64, u128, usizeUnsigned integers
f32, f64IEEE 754 binary32 and binary64
booltrue or false
charA Unicode scalar value (4 bytes)
()The unit type; one value ()
!The never type (a function that never returns)

Numeric literals admit underscore separators and type suffixes:

let a = 42;                      // inferred as i32 (default)
let b = 42u64;                   // explicit suffix
let c = 1_000_000;                // underscore separator
let d = 0xff;                     // hexadecimal
let e = 0b1010_1010;              // binary
let f = 0o755;                    // octal
let g = 3.14;                     // f64 (default)
let h = 3.14_f32;                 // f32 with suffix
let i = 1e6;                      // floating-point exponent

Without an explicit type, integer literals default to i32 and floating-point literals default to f64. The compiler infers more specific types from context:

let n: u8 = 42;          // forced to u8 by context
let m = 100u64;           // explicit suffix
let v: Vec<i64> = vec![1, 2, 3];   // i32 literals coerced via inference

The usize and isize are pointer-sized; they are the conventional types for indices and sizes (Vec<T>::len() returns usize).

bool and char

bool has two values:

let t = true;
let f = false;
let cond = 5 > 3;

bool is a distinct type; it does not implicitly convert to or from integers.

char is a Unicode scalar value (4 bytes, not a byte):

let c = 'A';
let emoji = '🎉';
let chinese = '世';
let escape = '\n';
let unicode = '\u{1F600}';        // Unicode escape

A char is not a byte; the byte representation of a string is accessible through &[u8] (a byte slice).

The unit type and the never type

() is the unit type — a type with one value, (). The conventional uses are:

  • The return type of functions that produce no value (similar to void in C).
  • The element type of empty tuples.
  • The placeholder in generic types.
fn announce(message: &str) -> () {     // explicit; the -> () is conventionally elided
    println!("{}", message);
}

fn announce(message: &str) {            // implicit; the same
    println!("{}", message);
}

! is the never type — the type of expressions that never produce a value. It is produced by panic!, loop (without break), process::exit, and similar:

fn fatal(message: &str) -> ! {
    eprintln!("{}", message);
    std::process::exit(1);
}

! admits being used as any type; the compiler treats “this code never returns” as compatible with any expected type. The mechanism admits if cond { value } else { panic!(...) } to type-check.

Tuples

A tuple is an anonymous fixed-arity record:

let pair: (i32, &str) = (42, "hello");
let triple = (1, "ok", true);
let unit: () = ();
let single: (i32,) = (1,);          // trailing comma for one-element tuple

// Access by index:
let n = pair.0;                       // 42
let s = pair.1;                       // "hello"

// Destructuring:
let (a, b) = pair;

Tuples are conventionally for fixed-shape, transient grouping. For named fields, struct is preferable.

Arrays and slices

An array is a fixed-size sequence of the same type:

let arr: [i32; 5] = [1, 2, 3, 4, 5];
let zeros = [0; 100];                  // 100 zeros
let n = arr[0];                          // indexed access
let len = arr.len();                     // 5

// Iteration:
for x in &arr {
    println!("{}", x);
}

The size is part of the type: [i32; 5] and [i32; 6] are different types. Arrays live on the stack (unless heap-allocated through a Box).

A slice &[T] is a borrowed view of a contiguous region:

let slice: &[i32] = &arr[1..4];        // [2, 3, 4]
let slice: &[i32] = &arr;                // borrows the whole array

fn sum(values: &[i32]) -> i32 {
    values.iter().sum()
}

sum(&[1, 2, 3, 4]);                     // works for arrays
sum(&vec![1, 2, 3, 4]);                 // works for Vec
sum(&array_slice);                       // works for slices

The slice does not own its data; it admits substantial code reuse — functions take &[T] rather than specific array sizes or Vec<T>.

Vec<T>, String, and Box<T>

Three principal owning types from std:

Vec<T>

A growable heap-allocated array:

let mut v: Vec<i32> = Vec::new();
v.push(1);
v.push(2);
v.push(3);

let mut v = vec![1, 2, 3, 4];          // macro
v.push(5);
v.pop();
let n = v[0];                            // indexed access
let n = v.get(10);                       // Option<&i32>; safe access

for x in &v {
    println!("{}", x);
}

let len = v.len();
let cap = v.capacity();

Vec<T> is the conventional Rust dynamic array; treated in Data structures.

String

A growable heap-allocated UTF-8 string:

let mut s = String::new();
s.push_str("hello");
s.push(',');
s.push(' ');
s.push_str("world");

let s = String::from("hello");
let s = "hello".to_string();
let s = format!("{} {}", "hello", "world");

Treated in Strings.

Box<T>

A heap-allocated owner of a T:

let boxed: Box<i32> = Box::new(42);
let n: i32 = *boxed;                    // dereference

// Conventional uses:
// 1. Recursive types:
enum List {
    Cons(i32, Box<List>),               // Box admits the recursion
    Nil,
}

// 2. Trait objects:
let drawable: Box<dyn Drawable> = Box::new(Circle::new(5.0));

// 3. Large values:
let large = Box::new([0u8; 10_000_000]);

Box<T> is the simplest smart pointer; treated in Smart pointers.

References

A reference is a borrowed pointer:

let x = 5;
let r: &i32 = &x;                       // shared (immutable) reference
let n: i32 = *r;                         // dereference

let mut y = 10;
let m: &mut i32 = &mut y;                // exclusive (mutable) reference
*m = 20;                                 // modify through the reference

The borrow rules:

  • At any time, either one mutable reference or any number of shared references — never both.
  • References must always be valid (the borrow checker enforces this through lifetimes).

The full treatment is in Ownership and borrowing and Lifetimes.

struct

A struct declares a user-defined product type:

struct Point {
    x: f64,
    y: f64,
}

// Tuple struct (fields by position):
struct Coord(f64, f64);

// Unit struct (no fields):
struct Marker;

// Construction:
let p = Point { x: 3.0, y: 4.0 };
let c = Coord(1.0, 2.0);
let m = Marker;

// Field access:
let x = p.x;
let first = c.0;

// Update syntax (non-trivial fields from another instance):
let p2 = Point { x: 10.0, ..p };       // x = 10.0, y from p

// Methods:
impl Point {
    fn new(x: f64, y: f64) -> Self {
        Point { x, y }
    }

    fn distance_to(&self, other: &Point) -> f64 {
        ((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
    }
}

struct is the principal user-defined type for grouped data. Treated in Data structures.

enum

An enum declares a sum type — a type whose value is one of several variants:

enum Shape {
    Circle(f64),                       // tuple variant
    Rectangle { width: f64, height: f64 },   // struct variant
    Square(f64),
}

let s = Shape::Circle(5.0);

// Pattern matching:
match s {
    Shape::Circle(r) => println!("circle with radius {}", r),
    Shape::Rectangle { width, height } => println!("{}x{}", width, height),
    Shape::Square(side) => println!("square with side {}", side),
}

enum is one of Rust’s most distinctive features. It admits the conventional sum types (Option<T>, Result<T, E>) plus arbitrary user-defined alternatives.

The two essential standard-library enums:

enum Option<T> {
    None,
    Some(T),
}

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Option<T> represents absence; Result<T, E> represents value-or-error. They are pervasive throughout Rust code; treated in Error handling.

The full treatment of enums and their use with pattern matching is in Pattern matching and Data structures.

Type aliases

The type keyword introduces an alias — a synonym for an existing type:

type Score = i32;
type Vector = Vec<f64>;
type Result<T> = std::result::Result<T, MyError>;     // re-typing the standard Result

fn compute() -> Result<i32> {
    // ...
}

Aliases are documentation; they do not introduce new types. For type-level distinctions, use newtype (a tuple struct with one field):

struct UserId(u32);                    // newtype; distinct from u32
struct OrderId(u32);                    // also distinct from UserId

fn user_only(id: UserId) {}
let id = UserId(42);
user_only(id);                          // OK
// user_only(OrderId(42));                // type error

The newtype is zero-cost (the runtime representation is identical to the wrapped type); the type system distinguishes them.

Conversion and casting

Rust does not perform implicit type conversion. The principal mechanisms:

as for primitive casts

let n: i32 = 42;
let m: i64 = n as i64;                  // primitive cast
let f: f64 = n as f64;
let b: u8 = 257 as u8;                  // truncates; result is 1

The as operator admits casts between primitive numeric types. The conversion is truncating for narrowing casts; the conventional safer alternative is the From/TryFrom traits.

From / Into traits

let s: String = String::from("hello");
let s: String = "hello".to_string();
let s: String = "hello".into();         // uses Into

let n: i64 = i64::from(42_i32);

For lossless conversions, From is the conventional choice. For potentially-lossy conversions, TryFrom returns a Result:

let n: u8 = u8::try_from(257_i32).unwrap_or(0);

parse

For string-to-number conversion:

let n: i32 = "42".parse().unwrap();
let f: f64 = "3.14".parse().unwrap();

parse() returns a Result<T, E>; conventional uses include error handling.

Type inference

Rust admits substantial type inference through the Hindley-Milner-style algorithm:

let x = 5;                              // i32 inferred
let v = vec![1, 2, 3];                  // Vec<i32> inferred
let mut map = HashMap::new();
map.insert("key", 42);                   // HashMap<&str, i32> inferred

let parsed: i32 = "42".parse().unwrap();  // i32 from the annotation
let parsed = "42".parse::<i32>().unwrap();  // turbofish

The compiler infers types from usage. Where the type cannot be inferred, an explicit annotation or a turbofish (::<T>) is required.

The conventional discipline:

  • Write type annotations on function signatures (the compiler does not infer those).
  • Skip annotations on local bindings when the type is obvious from the right-hand side.
  • Use turbofish (parse::<i32>()) for explicit type arguments to generic functions.

The Self type

Inside an impl block or a trait, Self refers to the type itself:

struct Point { x: f64, y: f64 }

impl Point {
    fn new(x: f64, y: f64) -> Self {    // Self == Point
        Self { x, y }                     // construct
    }

    fn origin() -> Self {
        Self { x: 0.0, y: 0.0 }
    }
}

Self is the conventional shorthand for “the implementing type”; admits substantial reuse if the type is later renamed or moved.

Generic types

Generic types take type parameters:

struct Pair<A, B> {
    first: A,
    second: B,
}

let p = Pair { first: 1, second: "hello" };

enum Maybe<T> {
    Some(T),
    None,
}

fn first<T>(v: &Vec<T>) -> Option<&T> {
    v.get(0)
}

Generics admit substantial code reuse with type safety. The full treatment is in Generics.

A note on what Rust types are not

Several features the C-family takes for granted are absent or different:

  • No nullOption<T> admits absence explicitly.
  • No subclass inheritance — traits and composition replace it.
  • No implicit conversionsas for primitive casts; From/Into for user-defined.
  • No exceptions in the typeResult<T, E> represents fallibility.
  • No reference counting by defaultRc<T>/Arc<T> admit it explicitly.
  • No mutable aliasing — the borrow rules forbid simultaneous mutable and shared references.

The combination — owned types, references with explicit lifetimes, sum types via enum, generics with traits — is the substance of Rust’s type system. Reading non-trivial Rust code requires fluency with each.