Better TypeScript... With JavaScript
Tags: typescript, react, javascript, tutorial
Published: 2020-07-27 19:35:11

[**NOTE:** The library that I reference throughout this post - `allow` - is now available in an NPM package.  You can find it here: https://www.npmjs.com/package/@toolz/allow]

In my previous post (https://dev.to/bytebodger/tossing-typescript-1md3) I laid out the reasons why TypeScript is, for me, a big #FAIL.  A lot of extra work in return for a false sense of security and few tangible benefits.

I won't rehash those arguments again.  You can browse through that article if you're interested. In this article, I'll be outlining my practical-and-tactical solution in a purely-JavaScript environment.

FWIW, I wrote an article somewhat-similar to this one back in March (https://dev.to/bytebodger/javascript-type-checking-without-typescript-21aa).  While the basis of my approach hasn't changed radically, the specifics of my implementation are quite different.

All of the code for _this_ article can be referenced in this single file:

https://github.com/bytebodger/spotify/blob/master/src/classes/allow.js

It's part of my **Spotify Toolz** project, although I'll also be porting it into my type-checking library.
<br/><br/>

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ecrm4l4zy8n8bx3g0niy.jpg)
## Type-Checking Goals
Without rehashing content from my previous articles, suffice it to say that there are several key factors that I find important in type-checking:

1. I care almost exclusively about ensuring type safety _at runtime_.  Telling me that your app _compiled_ means almost nothing to me.  Your app compiled.  I tied my shoes.  We didn't drive off a cliff.  Do we all get cookies??  If my app compiles, that's no guarantee that it _runs_.  If my app _runs_, it's guaranteed to compile.  So I focus on _runtime_.  <br/>

1. I care almost exclusively about ensuring type safety at the _interfaces_ between apps.  Those could be interfaces between my app and some outside data source - e.g., an API.  Or it could be the interface between one function and another.  It doesn't matter if the exchange reaches outside my app, or whether the exchange is entirely encapsulated by the app.  The point is that, if I know I'm getting "clean" inputs, there's a _much_ greater likelihood that any logic I've written inside the app will perform as expected.<br/>

1. Type-checking should be clean.  Fast.  Efficient.  If I have to spend countless hours trying to explain _functioning_ code to a compiler, then that type-checking is more of a hurdle than a feature.  This also means that type-checking should be as complete _as it needs to be_ - and no more.  In other words, if I'm receiving an object from an API response that contains 100 keys, but I'm only using _3_ of those keys, then I shouldn't have to define the other 97.  <br/>

1. "Defensive programming" should be kept to a minimum.  In my previous post, @somedood made a good point about the headaches of having to use a continual stream of `if` checks to ensure that we've received proper data.  I thoroughly understand this.  Any solution that requires constantly writing new `if` checks is - a non-solution.<br/><br/>

<br/>![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/dcqxnz9d6xgsnhyentw9.png)
## The Basic Approach
In my previous article, I outlined one scenario where we could be passing in a `number` - but would still need to check _inside_ the function to ensure that the argument is, indeed, a `number`.  The scenario looks like this:

```javascript
const createId = (length = 32) => {
  if (isNaN(length)) length = 32;
  // rest of function...
}
```

The simple fact is that, as long as we're targeting _runtime_ issues, there really is no way around this.  That's why I focus nearly _all_ of my validations on runtime validations.  Because I'm not interested in the faux-security that comes with successful compilation.
<br/>

> I want to know, in real-time, whether something will fail _at runtime_.

<br/>So my "answer" to this problem is that, if I can't _eliminate_ the inside-the-function-body validations, I at least want to make them clean, fast, and efficient.  With no manual need to craft fancy `if` conditions.

In the code linked-to above, I have a basic validation class that I've called `allow`.  `allow` contains a series of methods that check for various data types.  

One key difference in my _new_ approach is that each method is chained.  This means that I can perform all of my validations with a single line of code.  So whether a function has one argument or a dozen, I don't have copious LoC _inside_ the function spent on validating those inputs.

Another difference is that my latest approach doesn't return any validation values.  The methods simply `throw` on error or... nothing happens.  Which is exactly what I _want_ to happen.  

Of course, the code can be tweaked so that, in production, the "failure" results in some kind of silent error.  But the key is that, if a function receives "bad" data, then I _want_ that function to bail out in some way.

So the following examples will all look similar to this:

```javascript
const myFunction = (someBoolean = false, someString = '') => {
  allow.aBoolean(someBoolean).aString(someString);
  // rest of function...
}
```
&nbsp;

<br/><br/>
## The Simplest Validations
I call these "simple" because there's nothing to do but to pass in the value and see if it validates.  They look like this:

```javascript
// booleans
const myFunction = (someBoolean = false) => {
  allow.aBoolean(someBoolean);
  // rest of function...
}

// functions
const myFunction = (someCallback = () => {}) => {
  allow.aFunction(someCallback);
  // rest of function...
}

// React elements
const myFunction = (someElement = <></>) => {
  allow.aReactElement(someElement);
  // rest of function...
}
```

Nothing too magical about these.  `aBoolean()`, `aFunction()`, and `aReactElement()` will all fail if they do not receive their respective data types.
<br/><br/>

## Enums
Enums can be checked against a simple array of acceptable values.  Or you can pass in an object, in which case the object's values will be used to gather the acceptable values.

```javascript
// one of...
const statuses = ['open', 'closed', 'hold'];

const myFunction = (status = '') => {
  allow.oneOf(status, statuses);
  // rest of function...
}

const colors = {
  red: '#ff0000',
  green: '#00ff00',
  blue: '#0000ff',
}
const myFunction = (color = '') => {
  allow.oneOf(color, colors);
  // rest of function...
}
```
&nbsp;

<br/><br/>
## Strings
The simplest way to validate strings is like so:

```javascript
// string
const myFunction = (someString = '') => {
  allow.aString(someString);
  // rest of function...
}
```

But often, an _empty_ string is not really a _valid_ string, for the purposes of your function's logic.  And there may be other times when you want to indicate a `minLength` or a `maxLength`.  So you can also use the validation like so:

```javascript
// strings
const myFunction = (someString = '') => {
  allow.aString(someString, 1);
  // this ensures that someString is NOT empty
  // rest of function...
}

const myFunction = (stateAbbreviation = '') => {
  allow.aString(stateAbbreviation, 2, 2);
  // this ensures that stateAbbreviation is EXACTLY 2-characters in 
  // length
  // rest of function...
}

const myFunction = (description = '') => {
  allow.aString(description, 1, 250);
  // this ensures that description is not empty and is <= 250 
  // characters in length
  // rest of function...
}
```
&nbsp;

<br/><br/>
## Numbers
Like strings, numbers can be simply validated as being numerical-or-not.  Or they can be validated within a range.  I also find that I rarely use `allow.aNumber()` but I frequently use `allow.anInteger()`.  Because, in most cases where I'm expecting numbers, they really should be _integers_.

```javascript
// numbers
const myFunction = (balance = 0) => {
  allow.aNumber(balance);
  // can be ANY number, positive or negative, integer or decimal
  // rest of function...
}

const myFunction = (age = 0) => {
  allow.aNumber(age, 0, 125);
  // any number, integer or decimal, >= 0 and <= 125
  // rest of function...
}

const myFunction = (goalDifferential = 0) => {
  allow.anInteger(goalDifferential);
  // any integer, positive or negative
  // rest of function...
}

const myFunction = (id = 0) => {
  allow.anInteger(id, 1);
  // any integer, >= 1
  // rest of function...
}
```
&nbsp;

<br/><br/>
## Objects
This is not for defining _specific types_ of objects.  We'll cover that with `anInstanceOf`.  This only checks whether something fits the definition of being a generic "object" and, if you desire, whether the object is of a certain "size".

This also excludes `null` (which JavaScript classifies as an `object`) and arrays (which are also, technically, objects).  You'll see that there's a whole set of validations specifically for arrays in a minute.

```javascript
// objects
const myFunction = (user = {}) => {
  allow.anObject(user);
  // can be ANY object - even an empty object
  // rest of function...
}

const myFunction = (user = {}) => {
  allow.anObject(user, 1);
  // this doesn't validate the shape of the user object
  // but it ensures that the object isn't empty
  // rest of function...
}

const myFunction = (user = {}) => {
  allow.anObject(user, 4, 4);
  // again - it doesn't validate the contents of the user object
  // but it ensures that the object has exactly 4 keys
  // rest of function...
}
```
&nbsp;

<br/><br/>
## Instances
These validate the _shape_ of an object.  Please note that they don't validate the data types _within_ that shape.  Could it be extended to provide that level of validation?  Yes.  Do _I_ require that level of validation in my personal programming?  No.  So right now, it just concentrates on the existence of keys.

It will also validate recursively.  So if you have an object, that contains an object, that contains an object, you can still validate it with `anInstanceOf()`.  

`anInstanceOf()` requires an object, and a "model" object against which to check it.  Every key in the model is considered to be _required_.  But the supplied object can have additional keys that don't exist in the model object.  

```javascript
// instance of...
const meModel = {
  name: '',
  address: '',
  degrees: [],
  ancestors: {
    mother: '',
    father: '',
  },
}

let me = {
  name: 'adam',
  address: '101 Main',
  degrees: [],
  ancestors: {
    mother: 'mary',
    father: 'joe',
  },
  height: '5 foot',
}

const myFunction = (person = meModel) => {
  allow.anInstanceOf(person, meModel);
  // rest of function...
}
myFunction(me);
// this validates - me has an extra key, but that's ok
// because me contains all of the keys that exist in 
// meModel - also notice that meModel is used as the 
// default value - this provides code-completion clues
// to your IDE

let me = {
  name: 'adam',
  degrees: [],
  ancestors: {
    mother: 'mary',
    father: 'joe',
  },
  height: '5 foot',
}
myFunction(me);
// this does NOT validate - me is missing the address
// key that exists in meModel
```
&nbsp;

<br/><br/>
## Arrays
The simplest validation is just to ensure that a value _is_ an array.  Along with that validation, you can also ensure that the array is not empty, or that it is of a specific length.  

```javascript
// arrays
const myFunction = (someArray = []) => {
  allow.anArray(someArray);
  // rest of function...
}

const myFunction = (someArray = []) => {
  allow.anArray(someArray, 1);
  // this ensures that someArray is NOT empty
  // rest of function...
}

const myFunction = (someArray = []) => {
  allow.anArray(someArray, 2, 2);
  // this ensures that someArray contains EXACTLY 2 elements
  // rest of function...
}

const myFunction = (someArray = []) => {
  allow.anArray(someArray, 1, 250);
  // this ensures that someArray is not empty and is <= 250 
  // elements in length
  // rest of function...
}
```
&nbsp;

<br/><br/>
## Arrays Of...
It's often insufficient merely to know that something is an array.  You may need to ensure that the array contains elements _of a particular data type_.  In other words, you have arrays _of integers_, or arrays _of strings_, etc.

All of these come with `minLength`/`maxLength` optional arguments, so you can ensure that the arrays are non-empty, or are of a particular size.

```javascript
// array of arrays
const myFunction = (someArray = [[]]) => {
  allow.anArrayOfArrays(someArray);
  // rest of function...
}

// array of instances
const myFunction = (someArray = [meModel]) => {
  allow.anArrayOfInstances(someArray, meModel);
  // rest of function...
}

// array of integers
const myFunction = (someArray = [0]) => {
  allow.anArrayOfIntegers(someArray);
  // rest of function...
}

// array of numbers
const myFunction = (someArray = [0]) => {
  allow.anArrayOfNumbers(someArray);
  // rest of function...
}

// array of objects
const myFunction = (someArray = [{}]) => {
  allow.anArrayOfObjects(someArray);
  // rest of function...
}

// array of strings
const myFunction = (someArray = ['']) => {
  allow.anArrayOfStrings(someArray);
  // rest of function...
}
```
&nbsp;

<br/><br/>
## Real-World Examples
In my **Spotify Toolz** app, I'm currently using this runtime type-checking.  You can view that code here:

https://github.com/bytebodger/spotify

But here are some examples of what they look like in my functions:

```javascript
const getTrackDescription = (track = trackModel, index = -1) => {
  allow.anInstanceOf(track, trackModel).anInteger(index, is.not.negative);
  return (
	 <div key={track.id + index}>
		{index + 1}. {track.name} by {getTrackArtistNames(track)}
	 </div>
  );
}

const comparePlaylists = (playlist1 = playlistModel, playlist2 = playlistModel) => {
  allow.anInstanceOf(playlist1, playlistModel).anInstanceOf(playlist2, playlistModel);
  if (playlist1.name.toLowerCase() < playlist2.name.toLowerCase())
	 return -1;
  else if (playlist1.name.toLowerCase() > playlist2.name.toLowerCase())
	 return 1;
  else
	 return 0;
};

const addPlaylist = (playlist = playlistModel) => {
  allow.anInstanceOf(playlist, playlistModel);
  local.setItem('playlists', [...playlists, playlist]);
  setPlaylists([...playlists, playlist]);
}

const addTracks = (playlistId = '', uris = ['']) => {
  allow.aString(playlistId, is.not.empty).anArrayOfStrings(uris, is.not.empty);
  return api.call(the.method.post, `https://api.spotify.com/v1/playlists/${playlistId}/tracks`, {uris});
}
```

Every function signature is given runtime validation with a single line of code.  It's obviously more code than using _no_ validations.  But it's _far_ simpler than piling TS into the mix.
&nbsp;

<br/><br/>
## Conclusion
Does this _replace_ TypeScript??  Well... _of course not_.  But this one little library honestly provides far more value, _to me_, than a vast majority of the TS code that I've had to crank out over the last several months.  

I don't find myself "fighting" with the compiler.  I don't find myself having to write compiler checks _and_ runtime checks.  I just validate my function signatures and then I write my logic, content in the knowledge that, at runtime, the data types will be what I expect them to be.

Perhaps just as important, my IDE "gets" this.  For example, when I define an object's model, and then use it as the default value in a function signature, I don't have to tell my IDE that the `user` object can contain a `parents` object, which can contain a `mother` key and a `father` key.  

You may notice that there are empirical limits to the type-checking that I'm doing here.  For example, I'm validating the _shape_ of objects, but I'm not validating that every key in that object contains a specific _type_ of data.  I might add this in the future, but I don't consider this to be any kind of "critical flaw".  

You see, if I'm passing around shapes, and I can validate that a given object conforms to the _shape_ that I require, there's often little-to-no worry that the data _in those shapes_ is "correct".  Typically, if I've received a "bad" object, it can be detected by the fact that the object doesn't conform with the necessary _shape_.  It's exceedingly rare that an object is of the right shape - but contains unexpected data types.