Basic Types
17 min read
Rust has a rich type system with explicit types for different sizes and purposes. Unlike TypeScript’s single number type, Rust has many numeric types optimized for different use cases.
Quick Overview
Section titled “Quick Overview”Rust’s type system is:
- Explicit: Different types for integers, floats, etc.
- Safe: No implicit conversions that lose data
- Efficient: Choose the right size for your needs
Key types: Integers (i32, u8…), floats (f32, f64), booleans (bool), characters (char), tuples
TypeScript/JavaScript Example
Section titled “TypeScript/JavaScript Example”// TypeScript - Simple type systemlet integer: number = 42;let float: number = 3.14;let negative: number = -10;let big: number = 9007199254740991; // Max safe integer
let flag: boolean = true;let letter: string = "a"; // No char typelet tuple: [number, string] = [1, "hello"];
// All numbers are the same typelet x: number = 42;let y: number = 3.14;console.log(typeof x); // "number"console.log(typeof y); // "number"Key points:
- One
numbertype for all numbers - No distinction between integers and floats
- No control over size/memory usage
- Strings for single characters
Rust Equivalent
Section titled “Rust Equivalent”// Rust - Rich type systemlet integer: i32 = 42; // 32-bit signed integerlet float: f64 = 3.14; // 64-bit floatlet negative: i32 = -10; // Signed integerlet small: u8 = 255; // 8-bit unsigned (0-255)let big: i64 = 9_223_372_036_854_775_807; // 64-bit signed
let flag: bool = true; // Booleanlet letter: char = 'a'; // Unicode scalar value (4 bytes!)let tuple: (i32, &str) = (1, "hello"); // Tuple
// Different types for different purposeslet x: i32 = 42; // Integer typelet y: f64 = 3.14; // Float type// let z = x + y; // Error: can't add i32 and f64!let z = x as f64 + y; // Explicit conversionKey points:
- Multiple integer types (by size and signedness)
- Separate float types (f32, f64)
- Explicit conversions required
- True character type (Unicode)
Detailed Explanation
Section titled “Detailed Explanation”Integer Types
Section titled “Integer Types”Rust has 12 integer types based on:
- Signedness: Signed (
i) or unsigned (u) - Size: 8, 16, 32, 64, 128 bits, or architecture-dependent
Signed integers (can be negative):
| Type | Size | Range |
|---|---|---|
i8 | 8 bits | -128 to 127 |
i16 | 16 bits | -32,768 to 32,767 |
i32 | 32 bits | -2,147,483,648 to 2,147,483,647 |
i64 | 64 bits | -9,223,372,036,854,775,808 to … |
i128 | 128 bits | Very large range |
isize | arch | Depends on CPU (32 or 64 bits) |
Unsigned integers (only positive):
| Type | Size | Range |
|---|---|---|
u8 | 8 bits | 0 to 255 |
u16 | 16 bits | 0 to 65,535 |
u32 | 32 bits | 0 to 4,294,967,295 |
u64 | 64 bits | 0 to 18,446,744,073,709… |
u128 | 128 bits | Very large range |
usize | arch | Depends on CPU |
Default: When you write let x = 42;, Rust infers i32.
Examples:
// Signed integerslet a: i8 = -128; // Smallest i8let b: i8 = 127; // Largest i8let c: i32 = -2_000_000; // Default integer type
// Unsigned integerslet d: u8 = 255; // Largest u8let e: u16 = 65_535; // Largest u16let f: u32 = 4_000_000; // Common for large counts
// Architecture-dependentlet idx: usize = 10; // Used for array indices, sizeslet offset: isize = -5; // Used for pointer arithmeticNumber literals:
let decimal = 98_222; // Underscore for readabilitylet hex = 0xff; // Hexadecimallet octal = 0o77; // Octallet binary = 0b1111_0000; // Binarylet byte = b'A'; // u8 only (ASCII)
// Type suffixlet x = 42i32; // Explicitly i32let y = 100_u8; // Explicitly u8let z = 3.14f32; // Explicitly f32Floating-Point Types
Section titled “Floating-Point Types”Rust has two floating-point types:
| Type | Size | Precision | Default |
|---|---|---|---|
f32 | 32 bits | ~7 decimals | No |
f64 | 64 bits | ~15 decimals | Yes |
Examples:
let x = 2.0; // f64 (default)let y: f32 = 3.0; // f32 (explicit)
let pi: f64 = 3.14159265359;let e: f32 = 2.71828;
// Scientific notationlet large = 1e10; // f64; prints as 10000000000 (whole-valued floats omit the .0)let small = 1e-5; // f64; prints as 0.00001When to use f32 vs f64:
- f32: Graphics, game engines (GPU prefers f32)
- f64: Scientific computing, default (more precise)
Compare to TypeScript:
// TypeScript - one typelet x: number = 3.14; // Always IEEE-754 f64 (Rust's f64)let y: number = 2.0; // Same type; JS has no f32Boolean Type
Section titled “Boolean Type”Simple true/false:
let t = true; // Inferred as boollet f: bool = false; // Explicit type
// Common use in conditionalslet is_active = true;if is_active { println!("Active!");}
// Size: 1 byte (8 bits)Same as TypeScript:
let t: boolean = true;let f: boolean = false;Character Type
Section titled “Character Type”Rust’s char is a Unicode Scalar Value, not just ASCII!
let c = 'z'; // Inferred as charlet z: char = 'ℤ'; // Unicodelet heart = '\u{2764}'; // Emoji (a Unicode scalar)let chinese = '中'; // Chinese character
// Size: 4 bytes (32 bits)// Range: U+0000 to U+D7FF and U+E000 to U+10FFFFCompare to TypeScript:
// TypeScript - no char type, use stringlet c: string = "z";let emoji: string = "\u{2764}";Important: In Rust, 'a' is a char, "a" is a string!
let char_a = 'a'; // char typelet string_a = "a"; // &str type (string slice)Tuple Type
Section titled “Tuple Type”Group multiple values of different types:
let tup: (i32, f64, u8) = (500, 6.4, 1);
// Destructuringlet (x, y, z) = tup;println!("x: {}, y: {}, z: {}", x, y, z);
// Access by indexlet five_hundred = tup.0;let six_point_four = tup.1;let one = tup.2;
// Empty tuple (unit type)let unit: () = ();Compare to TypeScript:
// TypeScript tupleslet tup: [number, number, number] = [500, 6.4, 1];
// Destructuringlet [x, y, z] = tup;
// Access by indexlet five_hundred = tup[0];Tuple use cases:
- Return multiple values from functions
- Group related values temporarily
- Pattern matching (we’ll see later)
Unit type ():
fn do_something() { // No return value means returns ()}
fn explicit_unit() -> () { println!("Returns unit");}This looks like TypeScript’s void, but the two differ. TypeScript’s void is a type used to say “ignore whatever this returns” — there is no void value you can hold. Rust’s () (the unit type) is a real type with exactly one value, also written (). A function with no -> ... returns (), and you can bind it: let nothing: () = do_something(); is valid Rust.
Key Differences from TypeScript
Section titled “Key Differences from TypeScript”1. Multiple Number Types
Section titled “1. Multiple Number Types”TypeScript:
let x: number = 42;let y: number = 3.14;let z: number = x + y; // OKRust:
let x: i32 = 42;let y: f64 = 3.14;// let z = x + y; // Error: mismatched typeslet z = x as f64 + y; // Explicit conversion2. Overflow Behavior
Section titled “2. Overflow Behavior”TypeScript:
let x: number = 255;x = x + 1; // 256 (no problem)Rust (debug mode):
let mut x: u8 = 255;let one = std::env::args().count() as u8; // 1 at runtime, not a constantx = x + one; // Panics at runtime: "attempt to add with overflow"Rust (release mode):
let mut x: u8 = 255;let one = std::env::args().count() as u8; // 1 at runtime, not a constantx = x + one; // Wraps to 0 (two's complement)Note: Both operands above are computed at runtime on purpose. If you instead write
let x: u8 = 255; x + 1with two literals, the compiler folds the constants and rejects it outright witherror: this arithmetic operation will overflow— it never even gets to run.
Warning: This release-mode wrap is a documented logic error, not a feature to lean on. Relying on the silent wrap is discouraged; if you genuinely want wrapping, say so explicitly with
wrapping_addso the behavior is the same in debug and release:let x: u8 = 255;let y = x.wrapping_add(1); // 0, intentional and identical in debug + release
Explicit overflow handling:
let x: u8 = 255;
// Wrapping (always wraps)let y = x.wrapping_add(1); // 0
// Saturating (clamps to max)let y = x.saturating_add(1); // 255
// Checked (returns Option)let y = x.checked_add(1); // None
// Overflowing (returns tuple)let (y, overflowed) = x.overflowing_add(1); // (0, true)3. No Implicit Conversions
Section titled “3. No Implicit Conversions”TypeScript:
let x: number = 42;let y: string = String(x); // Explicit, but commonlet z = x + ""; // Implicit conversionRust:
let x: i32 = 42;// let y: String = x; // Error: can't convertlet y: String = x.to_string(); // Explicit
// Type casting with 'as'let a: i32 = 42;let b: f64 = a as f64;let c: u8 = 255;let d: i32 = c as i32;4. Character vs String
Section titled “4. Character vs String”TypeScript:
let c = "a"; // stringlet s = "hello"; // string (same type)Rust:
let c = 'a'; // char (4 bytes)let s = "hello"; // &str (string slice)// Different types!Common Pitfalls
Section titled “Common Pitfalls”Pitfall 1: Integer Overflow
Section titled “Pitfall 1: Integer Overflow”Problem:
fn add_one(x: u8) -> u8 { x + 1 // Panics in debug, wraps in release when x is 255}
fn main() { let y = add_one(255); println!("{y}");}Note: Here
xarrives as a function argument, so the overflow is only discovered at runtime: debug builds panic withattempt to add with overflow, release builds silently wrap to0. Writing the same overflow with two literals (let x: u8 = 255; x + 1) is caught at compile time instead — the compiler emitserror: this arithmetic operation will overflow.
Solution:
fn main() { let x: u8 = 255;
// Choose appropriate method let y = x.saturating_add(1); // 255 (clamps) let y = x.wrapping_add(1); // 0 (wraps)
// Or use larger type let x: u16 = 255; let y = x + 1; // 256}Pitfall 2: Mixing Integer Types
Section titled “Pitfall 2: Mixing Integer Types”Problem:
let x: i32 = 10;let y: i64 = 20;// let z = x + y; // Error: mismatched typesSolution:
let x: i32 = 10;let y: i64 = 20;let z = (x as i64) + y; // Convert x to i64Pitfall 3: Division Truncation
Section titled “Pitfall 3: Division Truncation”Problem:
let x = 5 / 2; // Result is 2, not 2.5!Why: Integer division truncates.
Solution:
let x = 5.0 / 2.0; // 2.5 (float division)let y = 5 as f64 / 2.0; // 2.5 (convert to float)Pitfall 4: Char vs String Confusion
Section titled “Pitfall 4: Char vs String Confusion”Problem:
let c: char = "a"; // Error: expected char, found &strSolution:
let c: char = 'a'; // Single quotes for charlet s: &str = "a"; // Double quotes for stringBest Practices
Section titled “Best Practices”1. Default to i32, Then Specialize
Section titled “1. Default to i32, Then Specialize”The Rust Book’s advice: start with i32 as your default integer. It is fast on modern CPUs and rarely the wrong choice. Reach for a different type only when you have a concrete reason — a domain bound, a memory budget, or an API that demands it.
Default unless you have a reason:
let count = 10; // i32 by inference — the sensible defaultlet id: u32 = 123456; // u32 because the ID is never negativelet size: usize = 1000; // usize because it indexes a collectionSpecialize when the domain or API calls for it:
let pixel: u8 = 255; // a color channel really is 0-255let port: u16 = 8080; // ports are 0-65535let idx: usize = 10; // indexing/length is always usizeTip: Choosing the smallest type that fits is not automatically better. Picking
u8for an age, for example, buys almost nothing and just invites overflow bugs as soon as you add to it. Optimize the size only where it matters (large arrays, packed structs, fixed protocol fields).
Rough guidance on when to deviate from i32:
u8: bytes, color channels, raw ASCIIu16: ports, values bounded above (e.g. HTTP status 100-599)i64/u64: timestamps, file sizes, values that outgrow 32 bitsusize/isize: collection indices, lengths, sizes — required by the standard library
2. Use Type Inference When Clear
Section titled “2. Use Type Inference When Clear”Don’t over-annotate:
let x: i32 = 42;let y: i32 = 10;let z: i32 = x + y;Let Rust infer:
let x = 42; // Inferred as i32let y = 10; // Inferred as i32let z = x + y; // Inferred as i32Annotate when needed:
let x: u8 = 42; // Need u8, not i32let y = Vec::new(); // Need to specify: Vec<i32>3. Use Suffixes for Clarity
Section titled “3. Use Suffixes for Clarity”Ambiguous:
let x = 42; // Is this i32, u32, i64?Clear intent:
let x = 42u8; // Explicitly u8let y = 1_000_000u32; // Explicitly u32let z = 3.14f32; // Explicitly f324. Handle Overflow Explicitly
Section titled “4. Handle Overflow Explicitly”Hope for the best:
let x: u8 = user_input + 10; // What if it overflows?Handle explicitly:
let x: u8 = user_input.saturating_add(10); // Clamps to 255// orlet x = user_input.checked_add(10) .expect("Overflow!");Real-World Example
Section titled “Real-World Example”Type-Safe Configuration
Section titled “Type-Safe Configuration”TypeScript:
interface Config { port: number; // Could be negative! maxConnections: number; timeout: number; // Milliseconds name: string;}
const config: Config = { port: 8080, maxConnections: 100, timeout: 5000, name: "MyServer",};Rust:
struct Config { port: u16, // 0-65535 (valid port range) max_connections: u32, // Only positive timeout_ms: u64, // Milliseconds, large range name: String,}
let config = Config { port: 8080, max_connections: 100, timeout_ms: 5000, name: String::from("MyServer"),};
// Can't set invalid port// let bad = Config { port: -1, ... }; // Won't compile!// let bad = Config { port: 70000, ... }; // Won't compile!The type system enforces validity!
Pixel Color Representation
Section titled “Pixel Color Representation”// RGB color (0-255 per channel)struct Color { r: u8, // Red: 0-255 g: u8, // Green: 0-255 b: u8, // Blue: 0-255}
let red = Color { r: 255, g: 0, b: 0 };let white = Color { r: 255, g: 255, b: 255 };
// Can't set invalid values// let bad = Color { r: 256, g: 0, b: 0 }; // Won't compile!u8 is perfect for this! Saves memory and enforces range.
Further Reading
Section titled “Further Reading”Official Documentation
Section titled “Official Documentation”Related Topics
Section titled “Related Topics”Exercises
Section titled “Exercises”Exercise 1: Choose the Right Type
Section titled “Exercise 1: Choose the Right Type”For each value, choose the most appropriate type:
// 1. Person's agelet age: /* what type? */ = 25;
// 2. HTTP status code (100-599)let status: /* what type? */ = 200;
// 3. Temperature in Celsius (-273.15 to ∞)let temp: /* what type? */ = -5.5;
// 4. Array indexlet idx: /* what type? */ = 10;
// 5. Unicode emojilet emoji: /* what type? */ = '\u{1F600}';Solutions
let age: u8 = 25; // 0-255 is enoughlet status: u16 = 200; // 100-599 fits in u16let temp: f32 = -5.5; // Float for decimalslet idx: usize = 10; // usize for indiceslet emoji: char = '\u{1F600}'; // char for UnicodeExercise 2: Fix Type Errors
Section titled “Exercise 2: Fix Type Errors”Fix this code:
fn main() { let x: i32 = 10; let y: f64 = 3.14; let z = x + y; println!("Result: {}", z);}Solution
fn main() { let x: i32 = 10; let y: f64 = 3.14; let z = x as f64 + y; // Convert x to f64 println!("Result: {}", z);}Exercise 3: Tuple Destructuring
Section titled “Exercise 3: Tuple Destructuring”Create a tuple with (name, age, score) and destructure it:
fn main() { // Create tuple let student = /* create tuple */;
// Destructure let (/* fill in */) = student;
println!("Name: {}, Age: {}, Score: {}", name, age, score);}Solution
fn main() { let student = ("Alice", 20, 95.5); let (name, age, score) = student; println!("Name: {}, Age: {}, Score: {}", name, age, score);}Exercise 4: Safe Arithmetic
Section titled “Exercise 4: Safe Arithmetic”Implement safe addition that doesn’t panic:
fn safe_add(a: u8, b: u8) -> u8 { // Use saturating or checked addition}
fn main() { println!("{}", safe_add(200, 100)); // Should be 255 println!("{}", safe_add(10, 20)); // Should be 30}Solution
fn safe_add(a: u8, b: u8) -> u8 { a.saturating_add(b) // Clamps to 255}
fn main() { println!("{}", safe_add(200, 100)); // 255 println!("{}", safe_add(10, 20)); // 30}Summary
Section titled “Summary”What you’ve learned:
- Rust has many integer types (i8, i32, u64, etc.)
- Two float types (f32, f64)
- Boolean and character types
- Tuples for grouping values
- No implicit conversions (use
as) - Explicit overflow handling
Key types:
// Integersi8, i16, i32, i64, i128, isize // Signedu8, u16, u32, u64, u128, usize // Unsigned
// Floatsf32, f64
// Othersbool // true/falsechar // Unicode scalar value() // Unit type(T, U) // TupleType conversion:
let x: i32 = 42;let y: f64 = x as f64; // Explicit castlet z: String = x.to_string(); // Method callChoose types wisely for memory efficiency and correctness!