Object-Oriented vs Functional Programming with Typescript

Jimmy Jiang
Dev Genius
Published in
8 min readOct 20, 2020

--

Object-orient or functional programming?

How about object-oriented vs functional programming?
If you guessed object-oriented is better you’re completely wrong :)
And if you guessed functional programming you’re way off base ;)

Debating programming paradigms at this level is like debating art.
There is always more than one way to solve a problem especially in Javascript and it’s great to debate these things but there are no absolutes.

There is going to be a trade-off for every decision that you make.
Let’s start by taking a look at some functional typescript code.

Pure Functions

The most important concept in functional programming is the concept of pure functions. This means that the output of your function should only depend on its inputs. For example:

let num = 123;
function toString(val) {
return val.toString();
}

Here we have a function called toString() which takes a value as its argument and then returns that value formatted as a string. We can make this an impure function by mutating the number variable directly.

let num = 123;
function toString(val) {
num = val;
...
}

This would be considered a side-effect and functional code should produce no side-effects. In addition they should not rely on any outside value to produce a return value. Pure functions are easier to test and also easier to reason about because you don’t have to think about anything happening outside of the function itself.

Immutable Data

Another core principle of functional programming is immutable data. Functional code is stateless meaning that when data is created it is never mutated. For example, we have:

const data = [1,2,3,4,5,6];

We can simulate this in Javascript by using Object.freeze on this array of numbers.

const data = Object.freeze([1,2,3,4,5]);

We could hack around this but it prevents us from doing things like array push which you wouldn’t do in a functional program. So obviously our data has to change somehow if we have a dynamic software application so you’ll often be passing functions as arguments to other functions.

const addEmoji = (val) => toString(val) + ' 😃';

So here, we have a typical first-order function which takes a value and returns a different value. In this case, just appending an emoji to a string.

Now a higher-order function is one that either takes a function as an argument or it returns a function itself. Javascript has some really nice built-in higher-order functions for arrays such as map. So instead of using a for loop, we can just pass in our function to map which will run our add emoji function on every element in the array and transform the value. So that gives us a very concise and elegant way to transform the values in an array.

const emojiData = data.map(addEmoji);
console.log(emojiData); // ['1 😃', '2 😃', '3 😃', '4 😃', '5 😃']

Another cool thing we can do is create functions that return functions.
This is very useful when you want to start with some base functionality and then extend it with some dynamic data.

Let’s imagine we are building a weather app and we need to append strings with certain emojis. We will start with a base function called appendEmoji and then use it to compose more complex functions.

const appendEmoji = (fixed) => (dynamic) => fixed + dynamic;

So in this case, the inner function takes both of the arguments and adds them together. We can use this to create more specialized functions that point to a specific emoji. For example:

const rain = appendEmoji('🌧️');
const sun = appendEmoji('️🌞');
console.log(rain(' today')) // ️🌧️ ️️today
console.log(sun(' tomorrow')) // ️🌞 tomorrow

Here, we have a rain function and a sun function then we can call this function with the string that we want the emoji appended to. The end result is some concise and readable code that doesn’t rely on any shared state that would make it difficult to test. That’s about as basic as it gets for functional programming and things get a lot more interesting when you have asynchronous data and side-effects and things like that.

So now let’s go ahead and compare this to object-oriented programming.

The first thing we will do is define a class which itself doesn’t really do anything but rather it serves as a blue print for instantiating objects so an instance of this emoji class will be an object with an icon property. Then the constructor method is special because it runs once when the object is instantiated. It will pass an argument to the constructor with the actual emoji icon and then we will set that equal to the property on this object. Like this:

class Emoji {
icon: string;

constructor(icon) {
this.icon = icon;
}
}
const sun = new Emoji('️🌞')console.log(sun) // Emoji { icon: '️🌞' }

The emoji class works similar to a function but we use the new keyword in front of it. And as you can see when we do that it creates an emoji object with an icon property of sun. In typescript there’s an easier way to do this because we have the concept of public and private members. So if we use the public keyword in front of the argument in the constructor, typescript will automatically know to set that as a public property on each object. When you declare a property or method public, it means it’s available to the class itself and any instances of the class.

class Emoji {
constructor(public icon) {}
}

That can be both a good and a bad thing. For example, we can simply change the icon by just mutating the value on its object.

const sun = new Emoji('️🌞')
sun.icon = '🌧️'
console.log(sun) // Emoji { icon: '🌧️' }

On one hand it’s very convenient but on the other hand if you have a lot of code doing this it can be hard to keep track of and hard to test effectively.

Typescript also provides some tools for us to improve the tone that we have when writing object-oriented code. For example, we can mark members as private so that they can only be used inside of this class definition. This means that we can separate our public API from internal logic for this class. For example if we want to make this icon value mutable, we can make it private and we will define a getter so the user can read the value but not change the value.

class Emoji {
constructor(private _icon) {}
get icon() {
return this._icon;
}
}

Another important thing to point out here is that class instances can have their own internal state. Let’s imagine we have a button where the user can toggle the emoji and maybe go back and forth between different states. This is a really simple thing to implement an object-oriented programming.

We will add another private property to this class called previous and then use a getter to retrieve that value then we will define a change method which will mutate the actual icon value on this instance.

class Emoji {    private _prev;    constructor(private _icon) {}    get icon() {
return this._icon;
}
get prev() {
return this._prev;
}
change(val) {
this._prev = this._icon;
}
}

When this happens we will first change the previous value to the current icon and then update the current icon to the new value.

const emoji = new Emoji('️🌞')console.log(emoji.icon, emoji.prev) // ️🌞 undefined

On this first console log, we get the sun icon and undefined and

emoji.change('🌧️');
emoji.change('🌪️');
console.log(emoji.icon, emoji.prev) // '🌪️', '🌧️'

If we mutate the state a couple times like above, you can see that our internal values on this class instance have changed.

Static Methods

The another cool thing you can do with classes is define static methods.

The unique thing about a method is that it’s on the class itself and not an instance of a class. So we will define a static method here which itself is actually a pure function and its job is simply to add one to the input argument.

class Emoji {
static addOneTo(val) {
return 1 + val;
}
}

Now we can use the Emoji class as a namespace to run this function.

Emoji.addOneTo(3);

Now we are going to switch gears and talk about composition versus inheritance for code reusability. This is an area where people tend to get very strong opinions and the actual definition of composition tends to be a little convoluted. Let’s go ahead and take a look at an example of inheritance.

class Human {    constructor(public name) {}    sayHi() {
return `Hello, ${this.name}`;
}
}
const patrick = new Human('Patrick Mullot')console.log(patrick.sayHi()); // Hello, Patrick Mullot

If we have a lot of other objects in our program that are similar but implement slightly different features based on what they’re designed to do. For example in a video game, you might have a human character and then a super human character that has all the human abilities but with a little something extra.
In typescript we can simply inherit all the functionality from the human class by saying SuperHuman extends human like this:

class SuperHuman extends Human {

heroName;
constructor(name) {
super(name);
}
}

Here we call super which will execute the code in the constructor of the parent class which in our case is just initializing this name property.

At this point we can go ahead and define an additional method called superpower().

class SuperHuman extends Human {

heroName;
constructor(name) {
super(name);
this.heroName = `HERO ${name}`;
superpower() {
return `${this.heroName} pops treys 🔥🔥🔥`
}
}
const steph = new SuperHuman('Steph Curry');console.log(steph.superpower()) // HERO Steph Curry pops treys 🔥🔥🔥

In this case the SuperHuman can still call all the methods that were defined in the parent class. For example:

console.log(steph.sayHi()) // Hello, Steph Curry

Inheritance can be great in the right situation but you want to avoid creating really deeply nested classes because it becomes very hard to debug when things go wrong somewhere in the middle. As an alternative we can use composition and there are actually multiple different ways we can apply this pattern. Another alternative is to concatenate objects together.

const hasName = (name) => {
return { name }
}
const canSayHi = (name) => {
return {
sayHi: () => `Hello, ${name}`
}
}

The idea here is that you decouple your properties or behaviors into objects or functions that return objects. We can then merge all these objects together into a final function that does everything that we need it to. This is usually referred to as a mixin patter and it’s just a certain type of multiple inheritance.

const Person = function(name) {
return {
...hasName(name),
...canSayHi(name)
}
}
const person = Person('Jeff')
console.log(person.sayHi()) // Hello, Jeff

This mixin pattern can be very powerful but in its current form we lose all of the ergonomics of class-based object-oriented programming. This might be a good or bad thing depending on who you ask but typescript actually gives us the flexibility to use mixins in a class-based format.

class CanSayHi {
name;
sayHi() {
return `Hello, ${this.name}`;
}
}
class HasSuperPower {
heroName;

superpower() {
return `${this.heroName} 🔥🔥🔥`;
}
}
class SuperHero implements CanSayHi, HasSuperPower {

heroName;
constructor(public name) {
this.heroName = `SUPER ${name}`;
}

sayHi: () => string;
superpower: () => string;
}

Here instead of extending the class, we implemented multiple classes. When you implement something you are only concerned about its interface and not its underlying code. It’s the apply mixins function that we defined at the very beginning that will actually take these interfaces and apply their code to this class. Final step is the requirement to call that apply mixins function with the base classes the first argument and the mixed in classes as the second argument.

applyMixins(SuperHero, [CanSayHi, HasSuperPower]);const ts = new SuperHero('TypeScript')console.log(ts.superpower()) // SUPER TypeScript 🔥🔥🔥

Now we can finally answer the question!

What’s your answer?

Conclusion

It is always up to the programmers or developers to choose the right programming language concept that makes their development productive and easy. :)

--

--