Learn How to Create Chainable Methods in Javascript With a Practical Example

Learn How to Create Chainable Methods in Javascript With a Practical Example

Chaining is a cool concept in programming, it is calling a method on a returned object without reassigning the value first. Suppose you have a users object:

const users = [
    {fname: 'Joseph', lname: 'Ada', age: 12},
    {fname: 'Bernard', lname: 'Rutsell', age: 81},
    {fname: 'Thompson', lname: 'Josaih', age: 41},
    {fname: 'Dayo', lname: 'Adeoye', age: 17}
]

And your task is to filter out users that are 18 and above. We can do this with the following lines of code which doesn't use method chaining:

// get all the ages from the user objects
const ages = users.map(user => user.age)
// only retain ages that are less than 18
const under18 = ages.filter(age => age < 18);

console.log(ages); // [ 12, 81, 41, 17 ]
console.log(under18) // [ 12, 17 ]

(If you don't know what map or filter does, please check out my other post where I explain these methods in plain English, A Common Sense Explanation of Javascript Array Methods. )

Javascript array methods can be chained together, this means that we can perform map operation and the filter operation without having to reassign returned values first:

const under18Chained = users
                        .map(user => user.age)
                        .filter(age => age < 18);

console.log(under18Chained); // [ 12, 17 ]

Since map returns an array, we can call filter on the returned array without having to reassign what map returned.

Most popular javascript frameworks implement chaining too, for example one can do this in jquery:

_$('button')
  .mousover(function () { console.log('mouse over') })
  .click(() => alert('clicked'));

In this post, we are going to write a Validator function that also allows method chaining, by the end of this post, we will be able to do this:

Validator(40)
            .num()
            .min(0)
            .max(5)

The validator function takes a number (40) and checks if it is a number,if it is not less than 0 and if it is not more than 5. In this case, 40 fails the second test (max(5))

Note: To follow along, you can type the code into the console of any modern browser, or use JS Bin . Or any other way of executing javascript code that you know :) Do follow along.

How Method Chaining Works

When we chained map and filter in the example above, it was because the map function returned an array - and all arrays have a filter method that can be called on them. filter also returns an array and we can chain other array methods to it too. So the key to chaining methods is what the methods return.

Every array in javascript inherits methods from the Array.prototype object so we have access to these array methods (filter, map, reduce and more) even though we didn't define them! An array in javascript is also an object.

users
    .map(user => user.age) // returns an array with `filter` as a property
    .filter(age => age < 18); // returns an array too!

If we want to implement our validator function, calling Validator with an argument, a number in this case, should return an object that has num() as a property. When will call num() it should also return an object that has min() as a property and min() should return an object that has max() has a property. The trick here is to let all of them return the same object, this object will have min, num, and max as it properties!

Validator(40) // returns an object with 'num' as a property
            .num() // returns an object with 'min' as a property 
            .min(0) // returns an object with 'max' as a property
            .max(5)

Step by step implementation

Let's define a Validator function that returns an object that has num as a method:

function Validator(elem) {
  return {
    num: function() {
      if (typeof elem === "number") return true;
      return false;
    }
  };
}

// call the function, logging the result to the console
console.log(Validator(40).num()); // true
console.log(Validator('A').num()); // false

The num method returns true or false, this is not really helpful. We want num to return the same object that Validator returns. To get this done, re-write Validator as follows:

function Validator(elem) {
  const actions = {};
  actions.num = function() {
    // `num` returns actions too when the test passes!
    if (typeof elem === "number") return actions; 
    return false;
  };

  // validator returns actions !
  return actions;
}

console.log(Validator(40).num()); // { num: [Function] }
// we can chain `num` without breaking things!
console.log(Validator(40).num().num()); // { num: [Function] }
console.log(Validator("A").num()); // false

Now we can chain num without breaking things! - but this only happens when the first num passes, we are currently returning false when it fails, calling false.num() will result in an error. To understand what I mean, try this:

// the first num() returns false, the second `num` call results in an error
console.log(Validator("A").num().num());

// TypeError: Validator(...).num(...).num is not a function...

To fix this, we need to return actions too when the test fails - but when the test fails, we will also include error messages.

function Validator(elem) {
  const actions = {};
  // create an array to hold error messages
  actions.errors = [];
  actions.num = function() {
    if (typeof elem === "number") return actions;
    else {
      actions.errors.push("Expected elem to be a number!");
      return actions;
    }
  };

  // validator returns actions !
  return actions;
}


console.log(Validator(40).num()); // { errors: [], num: [Function] }
console.log(Validator("A").num()); //  { errors: [ 'Expected elem to be a number!' ], num: [Function] }

Cool. Anyone using our function can check if the errors array is empty or not for validation. For example, suppose we are using this function to validate user input:

const age = 18 // this could come from a user submitted form

const { errors } = Validator(age).num();

if(errors.length < 1) {
    console.log('Valid')
    // no error occured! 
    // process the user input
} else {
    console.log('Error: ', errors[0]);
}

// Valid

Change the variable age to a string like const age = 'hello' and run the code, you get:

Error: Expected elem to be a number!

Now that you get the idea, let's implement the min method.

function Validator(elem) {
  const actions = {};
  actions.errors = [];
  actions.num = function() {
    if (typeof elem === "number") return actions;
    else {
      actions.errors.push("Expected elem to be a number!");
      return actions;
    }
  };

  actions.min = function(value) {
      if(elem >= value) return actions;
      actions.errors.push(`Number expected to be at least ${value}`)
      return actions;
  }

  // validator returns actions !
  return actions;
}

console.log(Validator(40).num().min(5)) 
// { errors: [], num: [Function], min: [Function] }

console.log(Validator(4).num().min(5))
// { errors: [ 'Number expected to be at least 5' ],
//  num: [Function],
//  min: [Function] }

console.log(Validator('a').num().min(5))
//{ errors:
//   [ 'Expected elem to be a number!',
//     'Number expected to be at least 5' ],
//  num: [Function],
//  min: [Function] }

Cool :)

Can you implement the max method? It is very similar to the min method. Try it out before checking the solution below:

function Validator(elem) {
  const actions = {};
  actions.errors = [];
  actions.num = function() {
    if (typeof elem === "number") return actions;
    // `num` returns actions too when the test passes!
    else {
      actions.errors.push("Expected elem to be a number!");
      return actions;
    }
  };

  actions.min = function(value) {
      if(elem >= value) return actions;
      actions.errors.push(`Number expected to be at least ${value}`)
      return actions;
  }

  actions.max = function(value) {
    if(elem <= value) return actions;
    actions.errors.push(`Number expected to be at most ${value} `)
    return actions;
  }

  // validator returns actions !
  return actions;
}

console.log(Validator(5).num().min(5).max(20)) 
// { errors: [], num: [Function], min: [Function], max: [Function] }


console.log(Validator(40).num().min(5).max(20)) 
// { errors: [ 'Number expected to be at most 20 ' ],
//  num: [Function],
//  min: [Function],
//  max: [Function] }

You get the point now. We have successfully written chainable methods! We are on our way to becoming javascript ninjas! :)

Adding More Functionalities

In Yoruba language, the word jara means to add more than strictly necessary, we have accomplished what this post was meant to do but we can do more. Let's add jara :).

We will let consumers of the function be able to specify an optional error message, something like this

Validator(5)
  .num('Please input a number')
  .min(5, 'Make sure the number is at least 5')
  .max(20, 'Too big! The number should not be more than 20')

If no error message was given, then we return the default error messages. Try implementing this before checking the solution! 1..2..go!

function Validator(elem) {
  const actions = {};
  actions.errors = [];
  actions.num = function(error = "Expected elem to be a number!") {
    if (typeof elem === "number") return actions;
    else {
      actions.errors.push(error);
      return actions;
    }
  };

  actions.min = function(value, error = `Number expected to be at least ${value}`) {
      if(elem >= value) return actions;
      actions.errors.push()
      return actions;
  }

  actions.max = function(value, error = `Number expected to be at most ${value}`) {
    if(elem <= value) return actions;
    actions.errors.push(error)
    return actions;
  }

  return actions;
}

console.log(
  Validator(40)
    .num('Please input a number')
    .min(5, 'Make sure the number is at least 5')
    .max(20, 'Too big! The number should not be more than 20')
)
//{ errors: [ 'Too big! The number should not be more than 20' ],
//  num: [Function],
//  min: [Function],
//  max: [Function] }

Did you come up with the same solution? Or did you use conditional statements (if...else)? Javascript now support default parameters and I have used them up there! Conditional statement solve the problem too, but the default parameters approach is cleaner.

Conclusion

Thanks for reading this far! I bet it's worth it. If you enjoy reading this post as much as I enjoyed writing it, (typing it...right? :) ) please share on social media, you can follow me on twitter @solathecoder .

Care to add some Jara?

Can you extend the Validator function to validate emails, booleans or/and alphanumerics? Try it out, you can drop your code in the comment section below, I will be glad to see what you come up with.

Happy Hacking!