Generics & Trait Bounds
TypeScript generics vs Rust generics
Section titled “TypeScript generics vs Rust generics”In TypeScript you write function identity<T>(x: T): T to make a function work for any type. Rust uses the same angle-bracket syntax: fn identity<T>(x: T) -> T. The key difference is that TypeScript’s generics are partially erased at compile time and rely on structural typing, while Rust’s are monomorphised — the compiler generates a completely separate, concrete function for every type you use. You pay zero overhead at runtime.
Trait bounds: constraining T
Section titled “Trait bounds: constraining T”TypeScript constrains generics with extends: <T extends Printable>. Rust constrains with trait bounds: <T: Display> (inline syntax) or a where clause (separate line, preferred for readability).
// TypeScript — constraint with extendsinterface Printable { toString(): string;}
function printTwice<T extends Printable>(value: T): void { console.log(value.toString()); console.log(value.toString());}
printTwice(42);printTwice("hello");use std::fmt::Display;
// Inline bound syntaxfn print_twice<T: Display>(value: T) { println!("{value}"); println!("{value}");}
// where clause — cleaner for multiple boundsfn print_and_debug<T>(value: T)where T: Display + std::fmt::Debug,{ println!("display: {value}"); println!("debug: {value:?}");}
fn main() { print_twice(42); print_twice("hello"); print_and_debug(3.14_f64);}Generic structs
Section titled “Generic structs”Just like TypeScript classes can be generic (class Stack<T>), Rust structs can be generic too. You add the type parameter after the struct name and repeat it in the impl block.
// TypeScript generic classclass Stack<T> { private items: T[] = []; push(item: T): void { this.items.push(item); } pop(): T | undefined { return this.items.pop(); } peek(): T | undefined { return this.items[this.items.length - 1]; }}
const s = new Stack<number>();s.push(1);s.push(2);console.log(s.pop()); // 2struct Stack<T> { items: Vec<T>,}
impl<T> Stack<T> { fn new() -> Self { Stack { items: Vec::new() } } fn push(&mut self, item: T) { self.items.push(item); } fn pop(&mut self) -> Option<T> { self.items.pop() } fn peek(&self) -> Option<&T> { self.items.last() }}
fn main() { let mut s: Stack<i32> = Stack::new(); s.push(1); s.push(2); println!("{:?}", s.pop()); // Some(2)}Multiple trait bounds and the + syntax
Section titled “Multiple trait bounds and the + syntax”You can require a type to implement several traits at once by joining them with +, just like TypeScript’s T extends A & B.
// TypeScriptfunction foo<T extends Serializable & Loggable>(x: T) { ... }
// Rust — inlinefn foo<T: Serialize + Debug>(x: T) { ... }
// Rust — where clause (preferred when bounds get long)fn foo<T>(x: T)where T: Serialize + Debug + Clone,{ ... }Try it
Section titled “Try it”use std::fmt::Display;
fn largest<T: PartialOrd>(list: &[T]) -> &T { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest}
fn describe<T: Display + std::fmt::Debug>(value: T) { println!("Display: {value}"); println!("Debug: {value:?}");}
fn main() { let numbers = vec![34, 50, 25, 100, 65]; println!("largest number: {}", largest(&numbers));
let chars = vec!['y', 'm', 'a', 'q']; println!("largest char: {}", largest(&chars));
describe(42_i32); describe("hello");}Compiling…