Macros
No direct TypeScript equivalent — but decorators come close
Section titled “No direct TypeScript equivalent — but decorators come close”Rust macros have no single TypeScript equivalent. The closest analogies are:
- TypeScript decorators (
@Injectable(),@Column()) — like#[derive(...)], they annotate code and trigger generated behaviour. - Template literal types / type-level metaprogramming — like
macro_rules!, they operate on syntax before runtime. - Babel / esbuild plugins — transformations applied at build time.
The key difference: Rust macros are hygienic and operate on the AST. They are part of the language, not a bolted-on annotation system, and they run at compile time with full access to the compiler’s token tree.
Two kinds of macros
Section titled “Two kinds of macros”| Kind | Syntax | What it does |
|---|---|---|
Declarative (macro_rules!) | macro_rules! my_macro { ... } | Pattern-matches on token trees and generates code |
Procedural (#[derive], #[attribute], function-like) | #[derive(Debug, Clone)] | Receives an AST node and emits new AST nodes |
This lesson focuses on macro_rules! (the simpler kind) and derive macros (which you have already used without thinking about them).
macro_rules! — declarative macros
Section titled “macro_rules! — declarative macros”macro_rules! works like a match expression for syntax. Each arm defines a pattern of tokens to match and a template of tokens to emit.
// TypeScript — no direct equivalent.// Closest: a generic utility function or a build-time code generator.function repeat<T>(value: T, times: number): T[] { return Array.from({ length: times }, () => value);}
console.log(repeat("hi", 3)); // ["hi", "hi", "hi"]
// TypeScript decorators (stage 3) annotate classes at runtime:// @Injectable() class MyService { ... }// macro_rules! — match on syntax, emit codemacro_rules! say_hello { // arm 1: no arguments () => { println!("Hello!"); }; // arm 2: one expression argument ($name:expr) => { println!("Hello, {}!", $name); };}
// #[derive] macro — auto-implement traits at compile time#[derive(Debug, Clone, PartialEq)]struct Point { x: f64, y: f64,}
fn main() { say_hello!(); say_hello!("Rustacean");
let p1 = Point { x: 1.0, y: 2.0 }; let p2 = p1.clone(); // Clone derived println!("{:?}", p1); // Debug derived println!("p1 == p2: {}", p1 == p2); // PartialEq derived}vec![] — a macro you use every day
Section titled “vec![] — a macro you use every day”vec![1, 2, 3] is itself a macro_rules! macro from the standard library. It expands to code that creates a Vec, calls push for each element, and returns the vector. Without it you would write:
let mut v = Vec::new();v.push(1);v.push(2);v.push(3);The println! and format! macros are also declarative macros — they parse the format string at compile time and generate type-safe formatting code.
Derive macros — the ones you use most
Section titled “Derive macros — the ones you use most”Every time you write #[derive(Debug, Clone, Serialize, Deserialize)], you are invoking a procedural macro. These are more powerful than macro_rules! because they receive the full AST of your struct or enum and emit arbitrary new code.
| Derive | What it generates |
|---|---|
Debug | fmt::Debug impl — enables {:?} printing |
Clone | clone() method |
PartialEq / Eq | == and != operators |
PartialOrd / Ord | <, >, <=, >= |
serde::Serialize / Deserialize | JSON (and other format) serialisation |
thiserror::Error | Display and From impls for error enums |
Try it
Section titled “Try it”macro_rules! say_hello { () => { println!("Hello!"); }; ($name:expr) => { println!("Hello, {}!", $name); };}
#[derive(Debug, Clone, PartialEq)]struct Point { x: f64, y: f64,}
fn main() { say_hello!(); say_hello!("Rustacean"); let p1 = Point { x: 1.0, y: 2.0 }; let p2 = p1.clone(); println!("{:?}", p1); println!("p1 == p2: {}", p1 == p2);}Compiling…