InterviewsVector
intermediateRare5 min read · Updated Jul 18, 2026

Implement _.partial(): Pre-fill Function Arguments

Implement lodash's _.partial in JavaScript with Symbol placeholders and this forwarding — and nail the partial-vs-curry-vs-bind comparison interviewers always ask.


The problem

Implement partial(fn, ...presetArgs): return a function with the leading arguments pre-filled — plus placeholder support so any positions can be preset, not just the leading ones.

Partial application is the simpler sibling of currying: you choose the argument split once, get back one function, and the next call runs fn — no arity counting, no chains. It's also two-thirds of bind (the argument half, without the this half), which makes "compare these three" the inevitable follow-up. Have the table ready.

Implementation

const _ = Symbol("placeholder"); // unforgeable — a string '_' collides with real data
 
function partial(fn, ...presetArgs) {
  return function (...laterArgs) {
    let n = 0;
    // fill placeholders left-to-right with incoming args…
    const args = presetArgs.map((arg) =>
      arg === _ && n < laterArgs.length ? laterArgs[n++] : arg
    );
    // …then append whatever's left
    args.push(...laterArgs.slice(n));
    return fn.apply(this, args); // forward this — partial must not sever it
  };
}

Verified behavior

const greet = (greeting, name, punct) => `${greeting}, ${name}${punct}`;
 
const hello = partial(greet, "Hello");
hello("Ada", "!");            // "Hello, Ada!"        — leading preset
 
const exclaim = partial(greet, _, _, "!");
exclaim("Hi", "Grace");       // "Hi, Grace!"         — trailing preset via holes
 
const greetAda = partial(greet, _, "Ada");
greetAda("Hey", "?");         // "Hey, Ada?"          — middle preset, extras append

The merge is one pass — fill holes in order, append leftovers. Unlike curry with placeholders, there's no completion rule and no recursion: unfilled holes simply arrive as the Symbol and it's the caller's bug — partial is deliberately dumb, and that simplicity is a feature to point out, not a gap to apologize for.

Where you see it in production

Specialization of general functions is everywhere; it just usually wears arrow-function clothes:

const log = partial(console.log, "[checkout]");   // tagged logger
const apiFetch = partial(fetchJson, BASE_URL);     // client bound to a host
onClick={() => deleteItem(item.id)}                // ← this IS partial application

That last line is the honest framing for 2026: arrows are inline partial application, and they won because they're readable and typecheck trivially. partial the utility earns its keep when the specialized function is created dynamically or in bulk (mapping over configs to build handler tables). Presenting arrows as the default and partial as the bulk tool is the right taste signal.

partial vs curry vs bind — the guaranteed follow-up

partial(fn, 1)curry(fn)(1)fn.bind(ctx, 1)
Returnsone function, split chosen oncea collector; keeps accepting args until arityone function
Next callalways invokes fninvokes only when arity is reachedalways invokes
thisforwarded from call siteforwardedfixed to ctx forever
Placeholdersyes (this article)yes (harder version)no — leading args only

The this row is the sharpest differentiator: partial preserves dynamic this (our wrapper is a function and applies this through), while bind freezes it. partial(obj.method, x) still needs the method attached to an object at call time or bound first — the lost-this bug applies to partials of methods exactly as it does to raw callbacks.

Edge cases interviewers probe

  • Placeholder never filledpartial(greet, _, "Ada")() passes the Symbol itself as greeting. Options: leave it (lodash's behavior — garbage in, garbage out), or throw on unfilled holes at call time. Either is defensible stated; silently substituting undefined is not.
  • More later-args than holes + presets — they append; matches how JS functions ignore extras.
  • Passing real undefined as a preset — works fine because the sentinel is a Symbol; with an undefined-as-placeholder design it would be indistinguishable — the sentinel-choice reasoning again (same as curry).
  • fn.length of the result — our wrapper reports 0 (rest params); lodash doesn't fix this either, but it breaks downstream arity-dependent tools like curry — a genuinely sneaky interaction worth naming.
  • Referential identity — each partial call makes a new function; in React-land that means memo-busting unless wrapped in useMemo/useCallback — the same identity discipline as debounce in React.

Common mistakes

  • String '_' as the placeholder — a legitimate "_" argument (CSS class, lodash import name!) silently becomes a hole.
  • Arrow function as the returned wrapper — severs this forwarding and quietly turns partial into half-broken bind.
  • shift()-based hole filling that mutates shared state across calls — the preset array must be treated as immutable (note the map builds a fresh array every call).
  • Conflating partial with curry in the explanation — the table is the cure.
  • Reaching for partial where an arrow is clearer — library-brain; the reviewer's version of this question is "when would you not use it?"

Follow-up questions

  • "Implement partialRight." — presets fill from the right: prepend laterArgs, append presets (mirror the merge). Lodash ships both; the exercise checks the merge generalizes in your head.
  • "Build bind using partial."bind(fn, ctx, ...preset) = partial(fn, ...preset) with apply(ctx) instead of apply(this) — plus the new-operator wrinkle that makes real bind harder.
  • "Type it in TypeScript?" — fixed splits type cleanly with tuple slicing (Parameters<F> spread); placeholders defeat static typing entirely — knowing that placeholder-partial is a dynamically-typed pattern is the mature answer.
  • "Partial an async function?" — nothing changes; it returns the promise through. The interesting bit is combining with retry/memoize wrappers — order of wrapping becomes an API-design conversation.
  • "Why did FP languages make this a language feature?" — in curried-by-default languages every call is partial application; JS bolts it on with closures — which is exactly why it's a closure interview question.

  • _.once()

    beginner

    Guarantee a function runs exactly once and returns its cached result forever after.

  • pipe()

    intermediate

    Compose functions left-to-right into a pipeline — the one-liner with a lot of interview depth.

  • memoize()

    intermediate

    Cache function results by argument key — and discuss cache-key strategy and memory trade-offs.