skip to content

Overloading vs Overriding

Two same-name mechanisms that get confused: overloading picks among signatures at compile time, overriding replaces an inherited method at run time. A perennial trap built on argument-type puzzles.

on this pageshow

questions

1

You are designing a library API whose surface will be consumed from languages that have no method overloading at all — Go, Python, JavaScript and Objective-C among them. How should you handle operations that would naturally be overloads, and what do overload sets turn into on the other side?

level: middleimportance: should knowfreq 30%

answer

  1. Overloading is resolved before runtime; one symbol survives
  2. Dynamic namespaces bind one value per name — later wins
  3. Objective-C: labels are part of the selector, so it is a distinct name
  4. Sniffing sees arity, not type
  5. Parameter object survives every boundary

basics

~20 s

An overload set is not a portable API concept — only a name plus an arity survives a boundary. Express variants as distinct names (Go's style), keyword or default parameters (Python), an options object (JavaScript), or argument labels (Objective-C). Never distinguish variants by argument type alone.

solid answer

~60 s

Overloading is a **compile-time** feature: the compiler picks one member from a set and emits a single mangled symbol. Nothing downstream of the compiler knows the set existed. That matters at every boundary. A dynamic language binds one value per name in a namespace, so a second definition replaces the first — overloading cannot exist there. Objective-C puts the argument labels *inside* the selector, so two "overloads" are simply two different names, which is why bridged APIs need explicit selector annotations. Go's standard library answers the same pressure with distinct intent-named functions rather than one name. A C-style or WebAssembly export table has one symbol per name; generators either suffix collisions or reject them. RPC and schema-driven interfaces key methods by string name, so two variants collide outright. So design for the weakest target: **name by intent, not by type**; keep at most one variant per arity if you overload at all, since runtime sniffing can see arity but not always type; and prefer a parameter object, which survives every boundary and is versionable.

go deeper

for a junior

Know that some languages have no overloading, and that giving variants distinct names or an options object is the portable choice.

for a middle

Explain that overloading is compile-time name resolution and that only name plus arity crosses a boundary; give the Objective-C selector and Go intent-naming examples.

for a senior

Turn it into concrete API rules — one variant per arity, parameter objects, no optional-plus-overload mixing — and connect them to generated bindings and RPC method naming.

for a principal

Argue the surface policy for a multi-language platform: the exported contract is names and shapes, overload sets are an internal ergonomic, and binding generators must never be allowed to invent names.

## Overloading is a compile-time construct An overload set is a group of members sharing a name, distinguished by their parameter lists. Resolution happens entirely at compile time: the compiler examines the static types of the arguments, ranks the applicable candidates, and emits a call to exactly one of them. The name that reaches the binary is typically *mangled* to encode the parameter types, precisely because the underlying linkage model allows only one symbol per name. Nothing after the compiler — no linker, no foreign-function interface, no RPC layer, no dynamic language runtime — knows that a set existed. That single fact explains every portability problem that follows. ## What each target offers instead **Go** has no overloading by deliberate choice, and no default parameters either. Its idiom is intent-naming: separate functions whose names say what varies, plus a variadic options pattern when the variation is a bag of settings. A generated binding that tries to project an overload set into Go must invent names, and invented names are worse than designed ones. **Python** binds one object per name in a namespace, so a second definition simply rebinds it. Its substitutes are default and keyword arguments, variadic parameters, and a runtime single-dispatch decorator that selects an implementation from the first argument's type. Its static-typing overload declarations are a checker-only fiction: they describe several call shapes over one real implementation, and nothing about them exists at runtime. **JavaScript** has one function object per name and no types at call time. Variants are distinguished by sniffing argument count or shape, which is brittle: absent arguments, null and undefined, and the fact that most numeric and string values are structurally indistinguishable from one another all conspire against it. The community answer is an options object. TypeScript's overload declarations are erased at emit and collapse to one implementation signature, so they help the checker and change nothing at runtime. **Objective-C** takes the most instructive position: the argument labels are part of the selector, so what looks like two overloads is literally two distinct method names. Languages that bridge to it must produce distinct selectors, which is why bridged declarations carry explicit selector annotations when the source language's overloads would otherwise collide. **Foreign-function and Wasm boundaries** expose a flat table of names. One symbol per name; generators either mangle, suffix, or fail. **RPC and schema-driven interfaces** — protocol-buffer services, JSON-RPC, OpenAPI operations — key on a string method name, so two variants of the same name cannot both exist. ## Design rules that survive every boundary 1. **Name by intent, not by type.** If the two variants do the same thing to different inputs, they can share a name inside one language, but the exported names should still say what differs. If they do *different* things, they should never have shared a name in the first place. 2. **At most one variant per arity.** Dynamic consumers can reliably observe how many arguments arrived; they cannot reliably observe what those arguments are. Two same-arity variants distinguished only by parameter type are indistinguishable on the other side. 3. **Watch the ambiguous values.** Null, undefined, integer-versus-floating numbers, and empty collections are the classic inputs that make runtime sniffing pick the wrong branch. 4. **Do not mix optional parameters with overloads.** Their interaction is where ambiguity errors and surprising default-value semantics live even inside a single language, and it is unprojectable outside it. 5. **Prefer a parameter object.** One name, one arity, one structured argument. It survives dynamic languages, RPC, schema evolution and code generation, and it is the only form that lets you add a variant without changing the call surface. 6. **Prefer named or labelled arguments where the language has them.** They give you the readability that overloading is usually reaching for, without needing a set. ## The cost, stated honestly You give up the ergonomics of one concept with one name, and you accept a longer surface: a family of related functions instead of a tight overload set. In exchange, the API means the same thing in every consumer, generated bindings do not invent names, and documentation never has to say "call the second overload" — a phrase that is itself the tell that the design has stopped being portable. ## The signal in an interview The insight being probed is that overloading is a *name-resolution* feature rather than a semantic one, and that name resolution is exactly the layer that boundaries discard. A candidate who reaches for the parameter object without first saying why the boundary discards the set has the recipe but not the reason.

  • TypeScript lets you declare several overload signatures for one function. Does that change anything at runtime?
    No. The declarations exist for the type checker and are erased at emit; a single implementation signature handles every call and must sort out the shapes itself, usually by counting arguments or testing them. So the checker enforces a discipline the runtime does not, and any consumer reached through JavaScript, an RPC boundary or generated bindings sees exactly one function.
  • Objective-C has no overloading, yet its APIs feel expressive. How?
    Because argument labels are part of the method name. What another language would express as several overloads becomes several fully spelled-out selectors, each self-describing at the call site. It trades a compact overload set for long but unambiguous names, and it is the reason bridged declarations sometimes need an explicit selector annotation: two source-language overloads would otherwise map to the same selector.

saying these in an interview costs you the question

  • Believing overloading exists at runtime rather than being resolved by the compiler
  • Distinguishing variants by parameter type alone in an API meant for cross-language use
  • Assuming type-checker-only overload declarations affect emitted code
  • Relying on argument sniffing to tell apart values that are structurally identical at runtime
  • Mixing optional parameters with overloads and expecting unambiguous resolution

context

open as a page