C.W.K.
Stream
Lesson 03 of 05 · published

Arrow vs Declared: This Is the Real Difference

~10 min · functions, arrow-functions, this, scope

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
"Arrow functions and declared functions are not stylistic variants. They make different decisions about this, and that decision is permanent."

The defining difference

A function declaration's this is dynamic. The value of this is determined by how the function is called: as a method (obj.fn()this is obj), as a plain function (fn()this is undefined in strict mode), via fn.call(other) (→ this is other), or via new fn() (→ this is the new instance).

An arrow function's this is lexical. It captures this from the surrounding scope at the moment of definition. Once captured, it never changes — calling .call(other) on an arrow function does nothing to this.

That's the whole rule. Every other arrow-vs-declared discussion is downstream of it.

When to reach for each

Arrow functions: callbacks, event handlers inside class methods, inline helpers, anywhere you want this to mean what it meant at the line you wrote. The 90%+ case in modern TypeScript.

Function declarations / expressions: object methods where this is supposed to be the object, class methods (which already get this-binding semantics automatically), constructors, and generator functions. Plus the rare case where you genuinely want this to be rebindable.

The class-method gotcha

Classes mix both. Class methods are function declarations under the hood, so their this is dynamic. That means passing obj.method as a callback loses this:

class Counter {
  count = 0;
  increment() { this.count++ }
}
const c = new Counter();
setTimeout(c.increment, 1000);  // ❌ `this` is undefined when called
setTimeout(() => c.increment(), 1000);  // ✅ arrow preserves `c`

The two common fixes: wrap the call in an arrow (above), or define the method as an arrow-function field (increment = () => { this.count++ } — captures this at construction time). The second has a cost — a new function per instance instead of one on the prototype — but it eliminates the binding issue.

The single rule that resolves 90% of these debates: if the function will be passed as a callback, make it an arrow (or arrow-field). If it's a method that callers will call on the object directly, use a regular method. If you're unsure, arrow is the safer default for callbacks.

Pippa's confession

cwkPippa's frontend uses arrow functions almost everywhere. The only declared functions are class methods (and React renders most components as plain functions, not classes). The arrow-field pattern shows up exactly when a method needs to be passed as a callback to a non-React API. The rule "use arrow unless you have a reason" has stayed stable across years.

Code

Dynamic vs lexical `this`·typescript
// Declared function — `this` is whatever the caller decides.
function announce() {
  console.log(this);   // depends on how you call announce
}

const obj = { name: 'Pippa', announce };
obj.announce();          // `this` is obj
const fn = obj.announce;
fn();                    // `this` is undefined in strict mode
announce.call({ name: 'Other' });  // `this` is { name: 'Other' }

// Arrow function — `this` is captured from where it's defined.
const announceArrow = () => {
  console.log(this);   // `this` from the enclosing scope at definition time
};

const obj2 = { name: 'Pippa', announceArrow };
obj2.announceArrow();    // `this` is NOT obj2 — it's whatever surrounded the arrow's definition
Class methods and callbacks — the canonical pain·typescript
// The class-method gotcha and two fixes.

class Counter {
  count = 0;

  // Regular method — `this` depends on caller.
  increment() {
    this.count++;
  }

  // Arrow-field — `this` is captured at construction.
  incrementArrow = () => {
    this.count++;
  };
}

const c = new Counter();

// Passing as callback:
setTimeout(c.increment, 100);          // ❌ `this` lost
setTimeout(() => c.increment(), 100);  // ✅ wrapped in arrow
setTimeout(c.incrementArrow, 100);     // ✅ arrow-field preserves `this`
setTimeout(c.increment.bind(c), 100);  // ✅ explicit bind, older style

// Trade-off: arrow-field allocates a new function per instance.
// Regular method lives on the prototype, shared by all instances.
// For most apps this allocation is irrelevant; for instance-heavy
// classes it can matter.

External links

Exercise

Write a Timer class with a start() method that calls this.tick() every second using setInterval. Implement it three ways: (1) regular method with setInterval(this.tick, ...), (2) regular method with setInterval(() => this.tick(), ...), (3) arrow-field for tick. Which compile? Which behave correctly at runtime? Why?
Hint
Version 1 loses this. Version 2 wraps the call in an arrow that captures this from start's scope. Version 3 captures this at the class instance level. All three patterns are real production patterns — try each, observe the differences, pick a default.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.