TypeScript and Best Practices: Modernizing the nth Function in JavaScript

Are you looking to modernize your JavaScript code? Look no further than our posts on updating code examples from books. In this third installment, we’ll show you how to modernize the nth function from "Functional JavaScript." We'll walk you through updating the code, including adding types with TypeScript. Follow along and learn how to update your code with the latest best practices.

Here are the original 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. Let TypeScript infer what type of array we are passing in would be much nicer. 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 to know the return type accurately. For example, if we call nth([1,2,3], 0), Typescript will know that the return type is of a 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.

Did you find this article valuable?

Support Travis Waith-Mair by becoming a sponsor. Any amount is appreciated!