Associated Functions and Constructors
21 min read
In TypeScript, a class gives you a built-in constructor and lets you mark helpers as static. Rust has neither keyword. Instead, both jobs are done by associated functions — functions that live inside an impl block but, unlike methods, do not take self. This file is about those functions: the Self::new constructors you will write constantly, named alternative constructors, and a first look at the builder pattern they unlock.
Quick Overview
Section titled “Quick Overview”An associated function is a function defined in an impl block that has no self parameter. You call it on the type with the :: path operator — User::new(...) — rather than on a value with the . operator. It is Rust’s equivalent of a TypeScript static method, and the conventional new associated function is as close as Rust gets to a constructor. The key thing for a TypeScript/JavaScript developer to absorb: Rust gives you no constructor for free. If you want User::new(...), you write it yourself.
TypeScript/JavaScript Example
Section titled “TypeScript/JavaScript Example”Here is a small User class in TypeScript. It uses the language’s built-in constructor, a couple of static factory methods that return new instances, and a static helper that returns a plain value rather than an instance.
// TypeScript - a class with a constructor and static factory methodsclass User { id: number; name: string; isActive: boolean;
// The built-in constructor: called via `new User(...)` constructor(id: number, name: string) { this.id = id; this.name = name; this.isActive = true; }
// A static factory: an alternative way to build a User static guest(): User { const u = new User(0, "guest"); u.isActive = false; return u; }
// A static helper that does NOT return a User static isValidId(id: number): boolean { return Number.isInteger(id) && id >= 0; }
// An ordinary (instance) method, for contrast — it uses `this` label(): string { return `#${this.id} ${this.name}`; }}
const alice = new User(1, "Alice");const visitor = User.guest();
console.log(alice.label()); // #1 Aliceconsole.log(visitor.isActive); // falseconsole.log(User.isValidId(-3)); // falseThree things to notice, because each maps to a distinct Rust idea:
new User(...)invokes theconstructor. Thenewkeyword and theconstructorslot are built in — you get them whether you write a constructor or not.User.guest()andUser.isValidId(...)arestatic: called on the class, not on an instance, and they have nothis.label()is an instance method: called on a value and usingthis.
Rust Equivalent
Section titled “Rust Equivalent”Rust splits these into two categories. Functions with self are methods (covered in impl-blocks.md); functions without self are associated functions. There is no new keyword and no automatic constructor — you write new as a plain associated function that returns Self.
#[derive(Debug)]struct User { id: u32, name: String, is_active: bool,}
impl User { // Associated function: no `self`. Called as `User::new(...)`. // `Self` is an alias for `User`, the type this `impl` block is for. fn new(id: u32, name: &str) -> Self { Self { id, name: name.to_string(), is_active: true, } }
// Another associated function: a named alternative constructor, // the equivalent of TypeScript's `static guest()`. fn guest() -> Self { Self { id: 0, name: String::from("guest"), is_active: false, } }
// An associated function that does NOT return Self — like a // TypeScript `static` helper that returns a plain value. fn is_valid_id(id: u32) -> bool { id != 0 }
// A method (takes &self), for contrast. Called as `value.label()`. fn label(&self) -> String { format!("#{} {}", self.id, self.name) }}
fn main() { // Associated functions are called on the TYPE with `::`, not on a value. let alice = User::new(1, "Alice"); let visitor = User::guest();
println!("{}", alice.label()); // method call, uses `.` println!("{}", visitor.is_active); println!("{}", User::is_valid_id(0)); // associated fn, uses `::` println!("{alice:?}");}Running it prints:
#1 AlicefalsefalseUser { id: 1, name: "Alice", is_active: true }The whole distinction comes down to one rule: a function with self is a method (call it with .); a function without self is an associated function (call it with ::). new, guest, and is_valid_id have no self, so they belong to the type, not to any particular value.
Note:
Self(capital S) is a type alias for the type theimplblock is for. Writing-> SelfandSelf { ... }instead of-> UserandUser { ... }is idiomatic: it keeps the constructor working unchanged if you rename the struct, and reads clearly as “return a value of my own type.”
Detailed Explanation
Section titled “Detailed Explanation”new is a convention, not a keyword
Section titled “new is a convention, not a keyword”In TypeScript, new User(1, "Alice") is special syntax: the new operator allocates an object and calls the constructor. In Rust there is no new operator and no special constructor slot. User::new(...) is calling a perfectly ordinary associated function that you happened to name new. You could name it create, make, or from_parts and it would work identically — new is simply the community convention for “the primary, no-frills constructor.”
This means the real way to build a struct is the literal struct syntax from structs.md:
let alice = User { id: 1, name: String::from("Alice"), is_active: true };A new associated function is just a convenience wrapper around that literal. It earns its keep by (a) hiding boilerplate (like defaulting is_active to true), and (b) being callable from other modules even when the struct’s fields are private — see Best Practices.
Associated functions vs. methods: the self test
Section titled “Associated functions vs. methods: the self test”Everything inside an impl block is an associated item. Among the functions, the dividing line is the first parameter:
| First parameter | Category | Called with | TypeScript analogy |
|---|---|---|---|
&self, &mut self, or self | method | value.method() | instance method (uses this) |
| (none) | associated function | Type::function() | static method (no this) |
Because an associated function has no self, the word self is simply not in scope inside its body. There is no instance to refer to — you are constructing or computing something for the type, not operating on an existing value.
Self and delegating between constructors
Section titled “Self and delegating between constructors”Self lets one constructor build on another. A named constructor can call Self::new to avoid repeating field logic:
#[derive(Debug)]struct Temperature { celsius: f64,}
impl Temperature { fn new(celsius: f64) -> Self { Self { celsius } }
// Delegates to `new` via `Self::new`, so the field logic lives in one place. fn from_fahrenheit(f: f64) -> Self { Self::new((f - 32.0) * 5.0 / 9.0) }
// An associated function that returns a plain value, not Self. fn absolute_zero_c() -> f64 { -273.15 }}
fn main() { let t = Temperature::from_fahrenheit(212.0); println!("{:.1}", t.celsius); // 100.0 println!("{}", Temperature::absolute_zero_c()); // -273.15}Output:
100.0-273.15Self::new(...) is the type-level counterpart of calling another method on self — except there is no self, so you reach for the function through the type alias Self.
Method-call syntax is sugar; :: is the real form
Section titled “Method-call syntax is sugar; :: is the real form”When you call a method, value.method() quietly desugars to Type::method(value) (the compiler also auto-inserts & or &mut as needed — see impl-blocks.md). Associated functions don’t get that sugar because there is no receiver value to put before the dot. So you always write the fully-qualified Type::function() form. That is why User::new(...) and alice.label() look syntactically different even though both are functions in the same impl block.
Default: a standard-library constructor convention
Section titled “Default: a standard-library constructor convention”Rust’s standard library has a trait, Default, whose single associated function default() returns a “sensible zero value” of the type. You rarely write it by hand: #[derive(Default)] generates it for you when every field is itself Default (numbers default to 0, String to "", bool to false, Option<T> to None).
// #[derive(Default)] writes `Settings::default()` for you.#[derive(Debug, Default)]struct Settings { verbose: bool, retries: u32, label: String,}
fn main() { let s = Settings::default(); println!("{s:?}");}Output:
Settings { verbose: false, retries: 0, label: "" }Settings::default() is an associated function (no self), just like new. The difference is that default() comes from a trait, so it is part of a shared, well-known interface; many APIs accept “anything that has a sensible default.” Reach for Default when “all fields zeroed” is a meaningful starting point, and for a hand-written new when construction needs arguments or non-trivial defaults. Full trait coverage is in Section 09.
Tip:
newanddefaultare not mutually exclusive. A common pattern is to deriveDefault, then havenew()(with no arguments) just callSelf::default(), so callers can use whichever they prefer.
Fallible Constructors: returning Result
Section titled “Fallible Constructors: returning Result”A TypeScript constructor can only throw on bad input — and a thrown error is invisible in the type signature. Rust associated functions return a value, so a constructor that can fail simply returns a Result instead of Self. The convention is to keep new infallible and use a descriptive name (or try_new) for the checked version.
#[derive(Debug)]struct Percentage { value: u8,}
impl Percentage { // A validated constructor returns Result instead of panicking. fn try_new(value: u8) -> Result<Self, String> { if value > 100 { Err(format!("{value} is greater than 100")) } else { Ok(Self { value }) } }}
fn main() { match Percentage::try_new(42) { Ok(p) => println!("ok: {p:?}"), Err(e) => println!("err: {e}"), } match Percentage::try_new(150) { Ok(p) => println!("ok: {p:?}"), Err(e) => println!("err: {e}"), }}Output:
ok: Percentage { value: 42 }err: 150 is greater than 100The signature try_new(value: u8) -> Result<Self, String> tells the caller, at compile time, that construction can fail and forces them to handle the failure. Contrast TypeScript, where new Percentage(150) throwing is something you can only discover by reading the constructor body or the docs.
Key Differences
Section titled “Key Differences”| Concept | TypeScript / JavaScript | Rust |
|---|---|---|
| Constructor | Built-in constructor, called with new | A plain associated function you write, named new by convention |
| Is a constructor provided for you? | Yes (implicit no-arg one if omitted) | No — you must write it |
| Static helper | static foo() | Associated function with no self |
| How you call it | Type.foo() (dot) | Type::foo() (path ::) |
| Distinguishing static from instance | The static keyword | Presence/absence of a self parameter |
| Returning the new value | Implicit (new returns the instance) | Explicit -> Self and an explicit Self { ... } |
| Failing construction | throw (invisible in the type) | Return Result<Self, E> (visible in the type) |
| “Zero value” factory | Ad hoc | The standard Default trait, often derived |
The conceptual headline: TypeScript bakes construction into the language (new + constructor); Rust treats construction as just another function. That sounds like extra work, and it is a little — but it pays off. There is no magic allocation step, fallible construction is expressed in the type system instead of via exceptions, and a type can have as many named constructors as it wants (new, guest, from_fahrenheit, …) instead of being limited to one overloaded constructor.
Common Pitfalls
Section titled “Common Pitfalls”Pitfall 1: Calling an associated function on a value with .
Section titled “Pitfall 1: Calling an associated function on a value with .”Coming from TypeScript, where instance.staticMethod() sometimes “works” by accident, it is tempting to call new (or any associated function) on an existing value.
struct Point { x: i32, y: i32,}
impl Point { fn origin() -> Self { Self { x: 0, y: 0 } }}
fn main() { let p = Point::origin(); // `origin` has no `self`, so it must be called on the TYPE, not a value: let q = p.origin(); // does not compile (error[E0599]) println!("{} {}", q.x, q.y);}The real compiler error points you straight at the fix:
error[E0599]: no method named `origin` found for struct `Point` in the current scope --> src/main.rs:15:15 | 1 | struct Point { | ------------ method `origin` not found for this struct...15 | let q = p.origin(); // does not compile (error[E0599]) | --^^^^^^-- | | | | | this is an associated function, not a method | help: use associated function syntax instead: `Point::origin()` | = note: found the following associated functions; to be used as methods, functions must have a `self` parameterFix: call it on the type — Point::origin(). The note even spells out the rule: to be callable as a method, a function must have a self parameter.
Pitfall 2: Trying to use self inside an associated function
Section titled “Pitfall 2: Trying to use self inside an associated function”If you reach for self in a function that has no self parameter, there is nothing for it to refer to.
struct Counter { count: u32,}
impl Counter { fn new() -> Self { // An associated function has no `self` in scope. Self { count: self.count } // does not compile (error[E0424]) }}
fn main() { let _c = Counter::new();}The real compiler error:
error[E0424]: expected value, found module `self` --> src/main.rs:8:23 |6 | fn new() -> Self { | --- this function doesn't have a `self` parameter7 | // An associated function has no `self` in scope.8 | Self { count: self.count } // does not compile (error[E0424]) | ^^^^ `self` value is a keyword only available in methods with a `self` parameterFix: an associated function builds a value from scratch (or from its arguments) — Self { count: 0 }. If you genuinely need access to an existing instance, you wanted a method, so add a self receiver and call it on a value.
Pitfall 3: Assuming a new exists by default
Section titled “Pitfall 3: Assuming a new exists by default”There is no automatically generated constructor. If you never wrote new, Type::new(...) does not exist.
#[derive(Debug)]struct Point { x: i32, y: i32,}
// No `impl` block defines `new`, so `Point::new` does not exist.
fn main() { let p = Point::new(1, 2); // does not compile (error[E0599]) println!("{p:?}");}The real compiler error:
error[E0599]: no function or associated item named `new` found for struct `Point` in the current scope --> src/main.rs:10:20 | 2 | struct Point { | ------------ function or associated item `new` not found for this struct...10 | let p = Point::new(1, 2); // does not compile (error[E0599]) | ^^^ function or associated item not found in `Point`Fix: either construct it with struct literal syntax — Point { x: 1, y: 2 } — or write your own fn new(x: i32, y: i32) -> Self. Unlike TypeScript, omitting a constructor gives you no constructor, not an implicit empty one.
Pitfall 4: Making new fallible by panicking instead of returning Result
Section titled “Pitfall 4: Making new fallible by panicking instead of returning Result”It is tempting to write fn new(...) -> Self and panic! on bad input, mirroring a TypeScript constructor that throws. That hides the failure from the caller’s type signature and turns a recoverable problem into a crash. If construction can legitimately fail on caller-supplied data, return Result<Self, E> from a try_new/parse/from_* function instead (see Section 08). Reserve panicking for cases that indicate a programmer bug, not bad input.
Best Practices
Section titled “Best Practices”- Name your primary constructor
new. It is the universal convention; readers expectType::new(...)to be the no-frills way to build a value. Don’t inventcreate/makeunlessnewwould be ambiguous. - Return
Self, not the spelled-out type. Write-> SelfandSelf { ... }. It is idiomatic and survives renames. - Use named alternative constructors freely.
from_fahrenheit,with_capacity,guest,parse— Rust has no constructor overloading, so multiple well-named associated functions are the idiomatic substitute. Follow thefrom_*convention when building from a single other value. - Make fallible construction return
Result. Express “this can fail on bad input” in the type, not via panics. Keepnewinfallible; addtry_new(or a parse-style function) for the checked path. - Derive
Defaultfor “all-zero” construction. If a sensible empty/zero value exists,#[derive(Default)]gives callersType::default()and plugs into the wider ecosystem. Havenew()delegate toSelf::default()when they coincide. - Reach for the builder pattern when a constructor has many optional parameters. Rust has no named or default arguments, so a six-argument
new(...)quickly becomes unreadable. A builder (below) is the idiomatic replacement. - Privacy makes constructors load-bearing. When a struct’s fields are private to its module, outside code cannot use struct-literal syntax — your
new/try_newbecomes the only entry point, letting you enforce invariants. See Section 12 for visibility.
Real-World Example
Section titled “Real-World Example”When a type has many optional settings, a single new(...) with a long argument list is painful — Rust has no default or named arguments, so callers must pass everything in order. The idiomatic answer is the builder pattern: an associated function returns a builder, fluent methods configure it, and a final build() produces the immutable value.
This is the production shape of the chaining teaser from impl-blocks.md. The associated function HttpRequest::builder(...) is the entry point; the rest of the API hangs off it.
#[derive(Debug)]struct HttpRequest { url: String, method: String, headers: Vec<(String, String)>, body: Option<String>, timeout_ms: u64,}
// The builder holds the in-progress configuration.#[derive(Debug)]struct HttpRequestBuilder { url: String, method: String, headers: Vec<(String, String)>, body: Option<String>, timeout_ms: u64,}
impl HttpRequest { // Associated function: the single entry point into the builder. // Returns the BUILDER, not a finished HttpRequest. fn builder(url: &str) -> HttpRequestBuilder { HttpRequestBuilder { url: url.to_string(), method: String::from("GET"), headers: Vec::new(), body: None, timeout_ms: 30_000, } }}
impl HttpRequestBuilder { // Each step takes `mut self` by value, mutates, and returns Self, // so calls chain. (Ownership flows through the chain.) fn method(mut self, method: &str) -> Self { self.method = method.to_string(); self }
fn header(mut self, key: &str, value: &str) -> Self { self.headers.push((key.to_string(), value.to_string())); self }
fn body(mut self, body: &str) -> Self { self.body = Some(body.to_string()); self }
fn timeout_ms(mut self, ms: u64) -> Self { self.timeout_ms = ms; self }
// Consumes the builder, producing the finished, immutable request. fn build(self) -> HttpRequest { HttpRequest { url: self.url, method: self.method, headers: self.headers, body: self.body, timeout_ms: self.timeout_ms, } }}
fn main() { let request = HttpRequest::builder("https://api.example.com/users") .method("POST") .header("Content-Type", "application/json") .header("Authorization", "Bearer token123") .body(r#"{"name":"Alice"}"#) .timeout_ms(5_000) .build();
println!("{} {}", request.method, request.url); for (key, value) in &request.headers { println!(" {key}: {value}"); } if let Some(body) = &request.body { println!(" body: {body}"); } println!(" timeout: {}ms", request.timeout_ms);}Output:
POST https://api.example.com/users Content-Type: application/json Authorization: Bearer token123 body: {"name":"Alice"} timeout: 5000msThis reads almost exactly like a TypeScript fluent builder that returns this, with one Rust twist: each step takes self by value and hands it back, so ownership flows down the chain and build() consumes the builder at the end — the builder cannot be reused afterward. Defaults (GET, empty headers, 30_000ms) live in builder(), and callers only override what they care about, in any order. That is exactly the ergonomics named/default arguments give you in TypeScript, reconstructed from associated functions and ownership.
Note: For real builders you usually pair this with
#[derive(Default)]on the builder and abuild()that returnsResultwhen required fields might be missing. Mature crates often generate the whole thing with thederive_builderorboncrate. The hand-written version here shows what those macros expand to.
Further Reading
Section titled “Further Reading”Official Documentation
Section titled “Official Documentation”- The Rust Book - Associated Functions
- Rust by Example - Associated functions & Methods
std::default::Default- Rust API Guidelines - Constructors are static, inherent methods (
new) - Rust Design Patterns - Builder
Related Sections in This Guide
Section titled “Related Sections in This Guide”- Structs — the struct-literal syntax a constructor wraps
- Methods and
implBlocks — associated functions’ counterpart: functions withself - Field Init Shorthand —
Self { id, name }shorthand and..otherupdate syntax used inside constructors - Associated Types & Consts — other items that live inside an
implblock - Tuple Structs — newtypes whose validated
try_newis a classic associated function - Ownership — why each builder step takes and returns
self - Variables and Mutability —
mut selfin builder steps - Error Handling — fallible constructors returning
Result - Collections —
Vec::newandVec::with_capacityare associated functions you already use
Exercises
Section titled “Exercises”Exercise 1: Multiple constructors for a Duration
Section titled “Exercise 1: Multiple constructors for a Duration”Difficulty: Easy
Objective: Write a primary new constructor plus named alternative constructors.
Instructions: Complete the impl block so that Duration::new(seconds) stores the seconds directly, while Duration::from_minutes and Duration::from_hours convert to seconds. All three are associated functions returning Self.
#[derive(Debug)]struct Duration { seconds: u64,}
impl Duration { fn new(seconds: u64) -> Self { // TODO }
fn from_minutes(minutes: u64) -> Self { // TODO: 1 minute = 60 seconds }
fn from_hours(hours: u64) -> Self { // TODO: 1 hour = 3600 seconds }}
fn main() { let a = Duration::new(90); let b = Duration::from_minutes(2); let c = Duration::from_hours(1); println!("{}", a.seconds); // 90 println!("{}", b.seconds); // 120 println!("{}", c.seconds); // 3600}Solution
#[derive(Debug)]struct Duration { seconds: u64,}
impl Duration { fn new(seconds: u64) -> Self { Self { seconds } }
fn from_minutes(minutes: u64) -> Self { Self { seconds: minutes * 60 } }
fn from_hours(hours: u64) -> Self { Self { seconds: hours * 3600 } }}
fn main() { let a = Duration::new(90); let b = Duration::from_minutes(2); let c = Duration::from_hours(1); println!("{}", a.seconds); // 90 println!("{}", b.seconds); // 120 println!("{}", c.seconds); // 3600}None of these take self — they are associated functions, called on the type with ::. Where TypeScript would force you into one overloaded constructor (or several static factories), Rust treats each named constructor as an ordinary function returning Self.
Exercise 2: A validated constructor that returns Result
Section titled “Exercise 2: A validated constructor that returns Result”Difficulty: Medium
Objective: Write a fallible constructor that rejects bad input via the type system instead of panicking.
Instructions: Complete Username::try_new so it trims whitespace, rejects an empty name with "username cannot be empty", rejects names longer than 20 characters with "username too long: N chars" (where N is the length), and otherwise returns Ok(Username(...)).
#[derive(Debug)]struct Username(String);
impl Username { fn try_new(raw: &str) -> Result<Self, String> { // TODO: trim, validate, return Ok(...) or Err(...) }}
fn main() { for raw in [" alice ", "", "this_name_is_definitely_way_too_long"] { match Username::try_new(raw) { Ok(u) => println!("ok: {u:?}"), Err(e) => println!("rejected {raw:?}: {e}"), } }}Solution
#[derive(Debug)]struct Username(String);
impl Username { fn try_new(raw: &str) -> Result<Self, String> { let trimmed = raw.trim(); if trimmed.is_empty() { return Err(String::from("username cannot be empty")); } if trimmed.len() > 20 { return Err(format!("username too long: {} chars", trimmed.len())); } Ok(Self(trimmed.to_string())) }}
fn main() { for raw in [" alice ", "", "this_name_is_definitely_way_too_long"] { match Username::try_new(raw) { Ok(u) => println!("ok: {u:?}"), Err(e) => println!("rejected {raw:?}: {e}"), } }}Output:
ok: Username("alice")rejected "": username cannot be emptyrejected "this_name_is_definitely_way_too_long": username too long: 36 charsReturning Result<Self, String> makes the possibility of failure part of the signature — callers cannot ignore it, unlike a TypeScript constructor that silently throws. Self(...) is the tuple-struct construction shorthand for Username(...).
Exercise 3: A small builder
Section titled “Exercise 3: A small builder”Difficulty: Medium/Hard
Objective: Build the builder pattern from scratch, starting with an associated builder() entry point.
Instructions: Give Pizza an associated function builder(size) that returns a PizzaBuilder (default: no toppings, no extra cheese). On PizzaBuilder, add chainable topping(name) (appends) and extra_cheese() (sets the flag), each taking mut self and returning Self, plus a build() that consumes the builder and returns a Pizza. Make the main below compile and run.
#[derive(Debug)]struct Pizza { size: String, toppings: Vec<String>, extra_cheese: bool,}
#[derive(Debug)]struct PizzaBuilder { // TODO: same fields as Pizza}
// impl Pizza { fn builder(size: &str) -> PizzaBuilder { ... } }// impl PizzaBuilder { topping / extra_cheese / build }
fn main() { let pizza = Pizza::builder("large") .topping("mushroom") .topping("olive") .extra_cheese() .build();
println!("{} pizza, extra cheese: {}", pizza.size, pizza.extra_cheese); println!("toppings: {:?}", pizza.toppings);}Solution
#[derive(Debug)]struct Pizza { size: String, toppings: Vec<String>, extra_cheese: bool,}
#[derive(Debug)]struct PizzaBuilder { size: String, toppings: Vec<String>, extra_cheese: bool,}
impl Pizza { fn builder(size: &str) -> PizzaBuilder { PizzaBuilder { size: size.to_string(), toppings: Vec::new(), extra_cheese: false, } }}
impl PizzaBuilder { fn topping(mut self, name: &str) -> Self { self.toppings.push(name.to_string()); self }
fn extra_cheese(mut self) -> Self { self.extra_cheese = true; self }
fn build(self) -> Pizza { Pizza { size: self.size, toppings: self.toppings, extra_cheese: self.extra_cheese, } }}
fn main() { let pizza = Pizza::builder("large") .topping("mushroom") .topping("olive") .extra_cheese() .build();
println!("{} pizza, extra cheese: {}", pizza.size, pizza.extra_cheese); println!("toppings: {:?}", pizza.toppings);}Output:
large pizza, extra cheese: truetoppings: ["mushroom", "olive"]Pizza::builder(...) is the associated-function entry point and returns the builder, not a Pizza. Each configuration step takes mut self and returns it so calls chain; build() takes self by value, consuming the builder to hand back the finished Pizza. This is the idiomatic stand-in for TypeScript’s optional/named arguments.