Skip to content

We never needed classes nor private properties, we have closures ​

Let’s create a dummy Chatter using a class and a closure to spot the differences.

Using a class ​

ts
class Chatter {
  constructor(firstName, lastName) {
    this.#firstName = firstName
    this.#lastName = lastName
  }

  get #name() {
    return `${this.#firstName} ${this.#lastName}`.trim()
  }

  wave(folk) {
    this.#sendMessage(`πŸ‘‹ ${recipient}`)
  }

  #sendMessage(message) {
    // fake implementation being fake
    console.log(`${this.name}> ${message}`)
  }
}

19 lines and hidden bugs because of this loose ends, picture this:

ts
const folks = ['Mary', 'Jane', 'Jamie', 'Jack']
const chatter = new Chatter('John', 'Doe')
folks.forEach(chatter.wave)

Here we need to bind chatter.wave to the chatter instance to make this line work. This is a fundamentally broken part of JavaScript, to be honest.

Using a closure ​

ts
function createChatter(firstName, lastName) {
  const getName = () => `${firstName} ${lastName}`.trim()
  const sendMessage = (message) => {
    // fake implementation being fake
    console.log(`${getName()}> ${message}`)
  }

  return {
    wave(folk) {
      sendMessage(`πŸ‘‹ ${recipient}`)
    },
  }
}

13 lines and no hidden bugs, this code will work as expected:

ts
const folks = ['Mary', 'Jane', 'Jamie', 'Jack']
const chatter = createChatter('John', 'Doe')
folks.forEach(chatter.wave)

Other goodies ​

1. A clean public programmatic interface ​

When we use a closure, it becomes immediately apparent that we only expose a single method wave.

When using a class, we need to browse the whole class before knowing what the public interface is.

2. A clean declaration order of private properties and methods ​

Take this example:

ts
class Chatter {
  #firstName = 'John'
  #sendMessage() { … }
  wave() { … }
  #lastName = 'Doe'
}

In a class, I can mix up everything, harming predictability of where to find stuff.

In a closure this is not possible, every private property must be declared at the top, no choice.

As for private methods, we have 2 choices: at the top like private properties, or at the bottom using function hoisting. I favor the latter.

ts
function createChatter(firstName, lastName) {
  // private property
  const name = `${firstName} ${lastName}`

  return {
    wave(…) { … }
  }

  // private method
  function sendMessage(…) {}
}

That way, the order is predictable: every private property is at the top, every private methods are at the bottom.

3. TypeScript: no typing redundancy ​

When implementing an interface using a class, we need to re-type methods πŸ˜’.

ts
interface Chatter {
  wave(folk: string): void
}

class SomeChatter implements Chatter {
  // double typing, duh. And this is required by the TypeScript compiler.
  wave(folk: string): void {}
}

// vs Closure
function createChatter(): Chatter {
  return {
    // properly typed, no double typing required.
    wave(folk) { … }
  }
}

4. No this to mess around ​

The this keyword and function binding is probably one of the most clunky feature of JavaScript, very error-prone even for experimented developers.

Even knowing about, I still got tricked here and there when using destructuring for instance:

ts
const chatter = new Chatter('John', 'Doe')
const { wave } = chatter
wave('Jack') // broken, `this` is not bound

Cons ​

So we know what we gain, but surely we must lose some things, right?

Well yes, sort of.

Using hoisted functions for both private and public interface ​

ts
function createChatter() {
  return {
    sendMessage, // public
    wave: (folk) => sendMessage(`πŸ‘‹ ${folk}`),
  }

  function sendMessage() {…}
}

When the object grows in size, it becomes hard to know which hoisted function is private and which is public.

But in my opinion, we should avoid using public methods in other public methods, it is a smell of harmful DRY (Don’t Repeat Yourself) because by doing so, we introduce a dependency – which is a first knot for spaghetti code.

Plus, this pattern is feasible using both closures and classes anyway.

Therefore, my recommendation: hoisted functions must only be private.