Skip to content

Traits

TypeScript interfaces are structural — Rust traits are nominal

Section titled “TypeScript interfaces are structural — Rust traits are nominal”

In TypeScript, interfaces use structural typing: if an object has the right shape (the right properties and methods), it satisfies the interface — no explicit declaration needed.

Rust traits use nominal typing: a type only implements a trait if you explicitly write impl Trait for Type. Matching the shape is not enough. This distinction has important consequences for library design and correctness.

TypeScript
// TypeScript — structural typing
interface Greet {
name(): string;
hello(): string;
}
// No explicit declaration — just match the shape
const english = {
name: () => "world",
hello() { return `Hello, ${this.name()}!`; }
};
function greetSomething(g: Greet) {
console.log(g.hello());
}
greetSomething(english); // works — english has the right shape
Rust
trait Greet {
fn name(&self) -> &str;
// Default method — free implementation, can be overridden
fn hello(&self) -> String {
format!("Hello, {}!", self.name())
}
}
struct English;
struct Thai;
impl Greet for English {
fn name(&self) -> &str { "world" }
// Uses the default hello()
}
impl Greet for Thai {
fn name(&self) -> &str { "โลก" }
// Override the default
fn hello(&self) -> String {
format!("สวัสดี {}!", self.name())
}
}
fn greet_something(g: &dyn Greet) {
println!("{}", g.hello());
}
fn main() {
greet_something(&English);
greet_something(&Thai);
}

Rust can auto-generate implementations for many common traits using #[derive(...)]. TypeScript has no equivalent — you implement interfaces manually or use class inheritance.

TypeScript
// TypeScript — implement interfaces manually or use classes
interface Printable {
toString(): string;
}
class Point implements Printable {
constructor(public x: number, public y: number) {}
toString(): string {
return `Point { x: ${this.x}, y: ${this.y} }`;
}
clone(): Point {
return new Point(this.x, this.y);
}
}
const p = new Point(1, 2);
console.log(p.toString());
Rust
// Rust — derive common traits automatically
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: f64,
y: f64,
}
fn main() {
let p1 = Point { x: 1.0, y: 2.0 };
let p2 = p1.clone(); // Clone derived
println!("{:?}", p1); // Debug derived — prints: Point { x: 1.0, y: 2.0 }
println!("equal: {}", p1 == p2); // PartialEq derived
}
trait Greet {
fn name(&self) -> &str;
fn hello(&self) -> String {
format!("Hello, {}!", self.name())
}
}
struct English;
struct Thai;
impl Greet for English {
fn name(&self) -> &str { "world" }
}
impl Greet for Thai {
fn name(&self) -> &str { "โลก" }
fn hello(&self) -> String {
format!("สวัสดี {}!", self.name())
}
}
fn main() {
let e = English;
let t = Thai;
println!("{}", e.hello());
println!("{}", t.hello());
}
How does Rust's type system differ from TypeScript's for trait/interface satisfaction?
What does Rust's coherence (orphan) rule prevent?
What does `#[derive(Debug)]` do?