Skip to content

Generics & Trait Bounds

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.

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
// TypeScript — constraint with extends
interface Printable {
toString(): string;
}
function printTwice<T extends Printable>(value: T): void {
console.log(value.toString());
console.log(value.toString());
}
printTwice(42);
printTwice("hello");
Rust
use std::fmt::Display;
// Inline bound syntax
fn print_twice<T: Display>(value: T) {
println!("{value}");
println!("{value}");
}
// where clause — cleaner for multiple bounds
fn 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);
}

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
// TypeScript generic class
class 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()); // 2
Rust
struct 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)
}

You can require a type to implement several traits at once by joining them with +, just like TypeScript’s T extends A & B.

// TypeScript
function foo<T extends Serializable & Loggable>(x: T) { ... }
// Rust — inline
fn foo<T: Serialize + Debug>(x: T) { ... }
// Rust — where clause (preferred when bounds get long)
fn foo<T>(x: T)
where
T: Serialize + Debug + Clone,
{ ... }
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");
}
What does Rust's monomorphisation mean for generic functions?
Which of these correctly adds two trait bounds to a generic parameter?
When should you prefer a `where` clause over inline bounds?
What is the TypeScript equivalent of Rust's `<T: Serialize + Debug>`?