JavaScript Type Checking... Without TypeScript
Tags: javascript, typescript, tutorial, codequality
Published: 2020-03-08 20:30:32

[**NOTE:** The concepts discussed in this article eventually evolved into a new approach with slightly different terminology.  That approach now comprises a package that I call `allow`.  You can find it here: https://www.npmjs.com/package/@toolz/allow]

There seems to be two crowds in the JavaScript community: those who use type-checking, and those who don't.  If you read that last sentence as "...those who use TypeScript, and those who don't" you can be forgiven for reading a _little_ more into the text than what was actually written.  Because, far too often, projects that don't use TypeScript have an appalling lack of type-checking in place.

That's why I wrote this concise little utility that you can find here:

https://github.com/bytebodger/type-checking

Go ahead and pop on over there when you have a chance.  It's only _one_ file (`is.js`).  It's all of 84 LoC.  But I use this little utility on an incredibly frequent basis.

[**Disclaimer:**  As you can imagine, with only 84 LoC, I'm not implying, in any way, that my silly little utility is any kind of replacement for TypeScript.  If you want/need _true_ type checking in your project, by all means, please reach for TypeScript (or Flow).  This is just a helpful utility for those times when you're working inside a project that does not - or cannot - use TypeScript.]

## The Problem

Nearly all of the programs that we write aren't actually singular, standalone programs.  Instead, our programs consist of dozens/hundreds/thousands of miniature programs which, in aggregate, make up our application.  You know what I'm talking about.  These smaller component programs are known as _functions_.

Each function is a (hopefully) tiny program in its own right.  It accepts an arbitrary list of zero-to-many inputs.  It returns a single output - or it generates no output at all.  Everything that happens _inside_ that function operates as its own little program.

Now I'm a big believer that type mutability in dynamically-typed languages is a _feature_, not a "bug".  If you want absolute certainty about all the types of all your variables at all times, then you shouldn't be programming in JavaScript in the first place.  There are statically-typed languages that are there for the taking.  And I can sometimes get kinda annoyed by the people who want to do everything they can to make JavaScript look/feel/act like C#.

But there's one area where I strongly believe that type certainty isn't a _preference_ or a _nice-to-have_.  This area is in the interface between functions.  It's a **must**, if we're to write solid, robust, bug-free functions.  In other words, it's nearly impossible to assure that our "mini-programs" (functions) will operate properly if we have no idea what _type_ of arguments are being passed into them.

## The Problem, Illustrated

```javascript
const updateUser = (userId, name, age, currentEmployee, children) => {
   // the update logic...
   return updateResult;
};
```

Despite the simple nature of this function, there's really a lot that's potentially going on here.  To update the user, we're accepting five separate arguments.  Without taking the time to delve through any logic that might be inside the function, there are numerous questions that crop up:

1. Is `userId` supposed to be an integer?  Or are we using some kind of alphanumeric (GUID) identifier, in which case this would be a string?<br/>

1. I _assume_ that `name` should be a string, although it's not out-of-the-question to believe that the function expects `name` to be _an object_.  Perhaps one that is formatted like so: `{first:'Tom', middle:'Tim', last:'Tum'}`.  Or maybe _an array_, like: `['Tom','Tim','Tum']`.<br/>

1. I _assume_ that `age` should be an integer, but will it accept decimals?  Will it accept `0`?<br/>

1. _Maybe_ `currentEmployee` is supposed to be a Boolean?  Or maybe it's a string that contains the name of the user's employee?  There's no way to know for certain.<br/>

1. Children "feels" like it should be an array - but again, there's no way to know that from the function signature.<br/>

So here we have two potential headaches:

There's little-to-no self-documentation going on in this function, so anyone invoking it has to either A. burn precious time reading through the entire function code to know exactly what's expected for each argument, or B. make a best-guess based upon the names of the arguments themselves.

And...

It's extremely difficult to write a robust function that will accept _any_ kind of input for _any_ of these five arguments without throwing an error or returning an aberrant value.  What happens if I pass in _an object_ for `userId`?  Or _an array_ for `age`?  Will the code fail gracefully?  

## (A Little) Help With Default Values

We can make this somewhat cleaner and easier to understand if we add default values to our arguments, like so:

```javascript
const updateUser = (userId = 0, name = '', age = 0, currentEmployee = false, children = []) => {
   // the update logic...
   return updateResult;
};
```

This definitely helps the casual developer to quickly grasp the _types_ of values that should be passed into this function.  We no longer have to guess about things like integer-vs-GUID userIds.  

But this does almost nothing to ensure the proper execution of the function itself.  That's because default values will only dictate the data type _when no value is supplied_.  If the caller does, in fact, provide a value for the argument, the supplied value is used, regardless of whatever data type is implied by the default values.

To put this in practical terms, the default argument values don't stop us from doing this:

```javascript
const updateUser = (userId = 0, name = '', age = 0, currentEmployee = false, children = []) => {
   // the update logic...
   return updateResult;
};

updateUser('007', {first:'Joe', last:'Blow'}, 'not saying', ['sure'], false);
```

In this case, we've made a real mess of the function invocation by chunking in a whole bunch of mismatched data types that our function probably wasn't expecting.  It doesn't matter that the default values _implied_ certain data types.  Since we actually _supplied_ our own data, JavaScript allowed us to pass in any data type we chose.

Here's another way that we can potentially screw up this function:

```javascript
const updateUser = (userId = 0, name = '', age = 0, currentEmployee = false, children = []) => {
   // the update logic...
   return updateResult;
};

updateUser(0, '', 0);
```

_Technically_, we provided the function with the correct data types that are implied in the default values.  But even though we accurately supplied `integer` \ `string` \ `integer`, there's a very good chance that this function invocation could fail or spawn some other kind of bug.  Because, while `0, '', 0` definitely satisfies the "spirit" of the function call, there's a very good chance that `0` is an invalid integer to use for `userId`, that `''` (empty string) is an invalid value to use for `name`, and that `0` is an invalid value to use for `age`.  

So unless the logic inside the function is robust, this might spawn some kind of error or bug - even though we _technically_ passed the proper data types into the function call.

At this point, I can almost _hear_ some of you thinking:

> None of this matters, because I'd never call my own function with the wrong types of data/values.

And that's great.  I'm glad that your own coding is perfect and flawless.  But once you've committed/merged the code for your function, you never technically know who's going to write new code (or alter existing code) to call that function.  In other words, once you put your function _out there_, into the wild, it has to stand on its own.  It needs to be as robust, bug-free, and foolproof as possible.

> The proper execution of your function should never be dependent upon the idea that the caller will invoke it in the "proper" way.

If there is any "downside" to functional programming, it's that you, as the function's writer, can control anything that happens _inside_ the function.  But you can't control how/when it's called.  

This is why I believe that JavaScript's dynamic typing is only a critical problem at the entrypoint to functions.  Because most functions _depend_ upon the data being presented in a certain format, and of a certain type.  

Sure... it's _possible_ to write all the logic _inside_ the function that you need to handle any-and-all types of inputs, but that can be overly laborious and bloat our otherwise sleek-and-efficient functions.

## One Potential Solution

As stated above in the disclaimer, the full/official/accepted way to address this is to use a heavy-duty strongly-typed system, like TypeScript or Flow.  But that's not always an option.  Sometimes you may not _want_ to go to that extreme.  Other times, you simply may not have the _option_ to add something like TypeScript to a legacy project.  

So are you stuck writing brittle functions?  Or writing bloated functions that painstakingly try to account for every possible input?  Hardly.  The repo that I linked to at the top of this article shows my homegrown solution.

It's really just a single file.  A class that I export and use as `is`.  I chose this name because it's very short, and it maps to the values that I expect back from all of `is`'s functions.  You see, every validation in the file returns a Boolean.  Every function checks to see whether a value conforms to a certain type.

In practical application, it looks like this:

```javascript
import is from './is';

const updateUser = (userId = 0, name = '', age = 0, currentEmployee = false, children = []) => {
   if (!is.aPositiveInteger(userId) || !is.aPopulatedString(name) || !is.aPositiveInteger(age) || !is.aBoolean(currentEmployee) || !is.anArray(children))
      return;
   // the update logic...
   return updateResult;
};
```

**Key Points:**

- If this looks a little wordy, please keep in mind that most functions have only one-or-two arguments.  The only reason this looks longer is because there are five separate arguments to check.<br/><br/>

- In the example above, I'm just bailing out of the function if any of the checks fails with a simple `return;`.  Obviously, depending upon the logic in your function, you may choose to follow a failed check with something like `return false;` or `return null;`.<br/><br/>

- I try to make the checks as specific as possible to the data that's needed inside the function.  For example, I don't do `if (!is.anInteger(userId))...` because `userId` should really be a _positive_ integer, and we don't want to have a value like `0` or `-482` passed in.  For the `name` value, we only want a _populated_ (non-empty) string.  It's not enough just to ensure that the supplied value is a string - because the empty string is still, technically, a string.  But the empty string is not a valid value.  But we're more lenient with regard to `children`.  Because it's perfectly fine for `children` to consist of nothing but an empty array.<br/><br/>

- Whenever one of these checks fails, it will throw a `console.error()` message for you to see in the dev tools.<br/><br/>

- Notice that an argument's default value, combined with the `is.()` check on the next line, tells us whether the argument is truly _required_.  We are supplying a default value for `userId` of `0`.  But the `is()` check ensures that the value is _greater than zero_.  This means, functionally speaking, that it's required for the caller to supply a `userId` value.  But `children` is not required.  It has a default value of `[]` and the `is()` check only ensures that the value is, indeed, an array.  So the function can be called without supplying any value for `children`.<br/><br/>

- There's certainly room to expand the list of validation in `is.js`.  For example, a function could be created to ensure that a value is an array _of strings_, or an array _of integers_, or an array _of objects_.  Of course, the more time you spend building out the validations in `is.js`, the more you have to ask yourself whether you should just be using a robust tool - like TypeScript.  So don't go too overboard with this.<br/><br/>

## Implementation

It's fairly self-explanatory.  But here are two tips that I use whenever deploying this in a non-TypeScript project:

1. Every argument, in every function, comes with a default value assigned.<br/>

1. The first line inside every function (that accepts arguments) consists of the `is()` checks needed to ensure that the supplied values conform with their expected data type.

That's it.  No other "rules" to abide by.  I hope this approach helps someone else as well.