The Three JS Features that Made Me a Functional Programmer

Adonis Gaitatzis
5 min readJul 18, 2023

A few years ago, the trend in JavaScript moved towards functional programming. I remained a fan of object-oriented programming (OOP) until last year, when finally I hit my tipping point.

First: Why I love OOP

I love object-oriented programming for four main reasons:

  1. Instances of similar variables can be easily scoped.
  2. Function names have a natural namespace under the class name.
  3. Functions with similar utility tend to be grouped together.
  4. Dependency injection and inheritance reduce redundancy.

These are all time-saving, bug-reducing features that improve code-reusability.

So What Changed?

Confusingly, JavaScript has several competing ways to express classes. This makes programming classes a inconsistent compared to other languages. But even that didn’t deter me from writing OOP in JavaScript.

The introduction of these three features are what finally turned me to the world of function programming in JavaScript:

  • Object destructuring and named properties
  • Import renaming
  • Arrow functions

Let’s see how these features combine to make OOP obsolete in JavaScript

Object destructuring and Named Properties

Named properties, a side-effect of JavaScript’s object destructuring, provides a way to pass some or all parameters to a function in any order. They are the JavaScript’s amazing answer to polymorphism.

// Polymorphism allows us to define multiple functions with the same name
class ExamplePerson {
id: string;
email: string;
constructor() { this.id = randomId(); }
constructor(id: string) { this.id = id; }
constructor(id: string, email: string) { this.id = id; this.email = email }
}

const example1 = new ExamplePerson(); // how do we know this is optional?
const example2 = new ExamplePerson(1); // what does "1" represent?
const example3 = new ExamplePerson(1, "email@example.com");

// Named parameters look like this
function createExamplePerson ({ id, email }) {
return { email, id: id || randomId() };
}
const example1 = createExamplePerson({});
const example2 = createExamplePerson({ id: 1 }); // we know that this is an id
const example3 = createExamplePerson({ id: 1, email: "email@example.com" });

TypeScript takes this to another level by adding type checking to named parameters:

// define the named parameter types
type Props = {
name: string;
age: number;
gender?: "male" | "female" | undefined;
}

// write a function that does something with this data
// note that gender is an optional parameter
const doSomethingWithUser = async ({ name, age, gender }: Props) => {
// do something with these values
}

// now we can execute that function with these parameters
doSomethingWithUser({ name: "John Doe", age: 32 });

// the ability to rearrange parameters and provide optional paramaters
// is more flexible than polymorphism
doSomethingWithUser({ age: 45, name: "Jane Doe", gender: "female" });

Named imports make refactoring easier. Need another optional variable? add it to the parameter type and you’re done.

If you do this with a class method, it’s much harder because traditionally you would need to write a new polymorphic method and inject parameters in all the existing function calls in a specific order. One new parameter could introduce breaking changes into all your dependencies.

Import Renaming

Older languages where functional programming was popular, such as Perl and C had a problem where function imports had ugly names to prevent namespace collision.

Let’s say your company had a custom LDAP identity management package that provides a sayHello() function which prints “Hello <name>”. The code to import and use that function would look something like this:

import company__ldap__person__sayhello from "company-ldap";

console.log(company__ldap__person__sayhello("John"));

By comparison, object oriented code provided a natural namespacing, which is much more readable, but you have to always look at the docs to know which functions the class provides:

// there is a `Person` class scoped inside the `ldap` package
import Person from "company-ldap";

// here the `sayHello` method is scoped inside of the `person` object
console.log(person.sayHello("John"));

But TypeScript can do something even more incredible: It can rename imports in the case of a name collision:

// these two functions from different packages have the same name
// but we can rename them for this script only
import { sayHello as ldapSayHello } from "ldap";
import { sayHello as mySayHello } from "./sayHello";

// now we can use both of them in the same script
console.log(ldapSayHello("John"));
console.log(mySayHello("John"));

Simple and elegant. And it makes tree-shaking much simpler.

Arrow Functions

JavaScript has a problem with the this property, which gets rescoped in a way that most people don’t understand.

A common problem in website development is to execute a function in response to a user interaction. Take for example button click which executes an AJAX call.

// respond to click on a button:
document.getElementById("#button").addEventListener("click", function() {
// `this` is the button element
console.log(this);
fetchJsonData().then(function(data) {
// `this` is the global context!
console.log(this);
});
});

The programmer may not realize that the this property is re-scoped inside the callback function. Although this makes sense, it’s very confusing to leave the scope of the button inside what feels like the same function block.

Traditionally, the solution was to assign const self = this before entering the callback function, which created a lot of redundancy and made refactoring a nightmare.

Arrow functions solves this by not reassigning this.

Using arrow functions, we can continue to use refer to the button even inside the callback:

document.getElementById("#button").addEventListener("click", function () {
// `this` is the button element
console.log(this);
fetchJsonData().then(data => {
// `this` is still the button element!
console.log(this);
});
}

Of course, if we use arrow functions everywhere, then this is always the global context, so you never get confused:

document.getElementById("#button").addEventListener("click", () => {
// `this` is the global context
console.log(this);
fetchJsonData().then(data => {
// `this` is still the global context!
console.log(this);
});
}

Conclusion

These three features make JavaScript and TypeScript a simple, elegant, and flexible language. It makes functional programming more modular, and easier to read and understand than object oriented programming. It reduces the complexity of refactoring, and reduces the amount of documentation needed for any developer to become familiar with their scope of work.

If you are still stuck on object-oriented programming, try these features and see if you are convinced.

--

--

Adonis Gaitatzis

Is a technology and branding nerd who can synthesize any business need into a technology solution. Fun projects have included brain imaging tech & mesh routers.