Modernizing Examples: The nth Function

Photo by Melanie Pongratz on Unsplash

Modernizing Examples: The nth Function

Travis Waith-Mair's photo
Travis Waith-Mair

Published on Dec 3, 2021

5 min read

Subscribe to my newsletter and never miss my upcoming articles

This is the 3rd installment in what is starting to become a series of posts where we modernize code examples from books. In the previous post, we built some utility functions to create a parseAge function. The next example in the "Functional JavaScript" book uses those same utility functions to build an nth function. So let's modernize this code together.

Here is the orginial examples:

function isIndexed(data) {
  return _.isArray(data) || _.isString(data);
}

function nth(a, index) {
  if (!_.isNumber(index)) fail("Expected a number as the index");
  if (!isIndexed(a)) fail("Not supported on non-indexed type");
  if (index < 0 || index > a.length - 1) fail("Index value is out of bounds");

  return a[index];
}

function second(a) {
  return nth(a, 1);
}

second("abc"); // b
nth([1, 2, 3], 0); // 1
nth({}, 2); // Error: Not supported on non-indexed type

The nth function takes in an indexable value and an index. It does some guards to make sure you have actually passed in an indexable value and an index that is in bounds, and if everything is safe, it will return the value at the index. Since we already went over the fail function in the previous post, I have left that code out.

Let's start with the isIndexed function. This uses two utilities from underscore.js to confirm if the value is either an array or a string. There isn't an "array" type in JavaScript that you can check using the typeof operator. If you call typeof [], you will get "object." This is because arrays are just objects under the hood. In the past, we relied on utility functions, like _.isArray to reliably check if value actually is an array and not just an object.

Luckily, we have a static method on the Array object called isArray that does this without needing a utility library. You can learn more about Array.isArray over at MDN.. Also, in the last post, we created our own assertIsString function. We can steal that logic and replace both of these checks, like this:

function isString(value) {
  return toString.call(value) === "[object String]";
}

function isIndexed(data) {
  return Array.isArray(data) || isString(data);
}

As for the nth function, the only thing we have in there that should be updated is the _.isNumber function from underscore. We can adapt our isString function to be an isNumber function with one tweak:

function isNumber(value) {
  return toString.call(value) === "[object Number]";
}

So we can update the example to look like this:

function isNumber(value) {
  return toString.call(value) === "[object Number]";
}

function isString(value) {
  return toString.call(value) === "[object String]";
}

function isIndexed(data) {
  return Array.isArray(data) || isString(data);
}

function nth(a, index) {
  if (!isNumber(index)) fail("Expected a number as the index");
  if (!isIndexed(a)) fail("Not supported on non-indexed type");
  if (index < 0 || index > a.length - 1) fail("Index value is out of bounds");

  return a[index];
}

function second(a) {
  return nth(a, 1);
}

second("abc"); // b
nth([1, 2, 3], 0); // 1
nth({}, 2); // Error: Not supported on non-indexed type

Now, let's add types. Our is functions are simple enough. Each one takes an unknown value and determines if it is something, so let's type them like this"

function isNumber(value: unknown) {
  return toString.call(value) === "[object Number]";
}

function isString(value: unknown) {
  return toString.call(value) === "[object String]";
}

function isIndexed(data: unknown) {
  return Array.isArray(data) || isString(data);
}

Now let's add the types to our nth function. In the previous post, I typed the function as unknown, partially because I wanted to keep the spirit of how the function was written, but partially so we could learn more about assertion functions. In a real TypeScript code base, we would write the types much more explicitly.

Our nth function takes two arguments: a and index. index is simple enough to type because it needs to be of type number. a could either be a string or an array. So first, let's create a type called Indexed:

type Indexed = Array<unknown> | string;

This essentially says, that Indexed could be an array of unknown values or a string. Now we could do something like this:

function nth(a: Indexed, index: number) {}

But this will force the value of a to be an array of unknown values if we pass in an array. It would be much nicer to let TypeScript infer what type of array we are passing in. Luckily we can do that using generics. Let's look at the updated types:

function nth<T extends Indexed>(a: T, index: number): T[number] {}

Let's break that down. First, we are saying that nth takes a generic value of T that must extend the Indexed type we declared. Then we say that a is of type T and that we are returning T[number] or the value at the index of T.

By having T extend Indexed, we are now letting Typescript infer the array's values so it can then accurately know what the return type is. For example, if we call nth([1,2,3], 0), Typescript will know that the return type is of type number.

The second function below can be typed the same way, except it wont need an explicit return type since it can infer that from the nth function we are calling. So now let's update the entire example using TypesScript:

function isNumber(value: unknown) {
  return toString.call(value) === "[object Number]";
}

function isString(value: unknown) {
  return toString.call(value) === "[object String]";
}

function isIndexed(data: unknown) {
  return Array.isArray(data) || isString(data);
}

type Indexed = Array<unknown> | string;

function nth<T extends Indexed>(a: T, index: number): T[number] {
  if (!isNumber(index)) fail("Expected a number as the index");
  if (!isIndexed(a)) fail("Not supported on non-indexed type");
  if (index < 0 || index > a.length - 1) fail("Index value is out of bounds");

  return a[index];
}

function second<T extends Indexed>(a: T) {
  return nth(a, 1);
}

second("abc"); // b
nth([1, 2, 3], 0); // 1
nth({}, 2); // TypeError

There you have it, a fully modernized and typed example from the classic "Functional JavaScript." I highly recommend checking it out, and I'll see you next time.

 
Share this