DEV Community

Kirill
Kirill

Posted on

Navigating the Maze of Type-Guarding in JavaScript

Introduction

JavaScript, renowned for its flexibility, introduces unique challenges when it comes to type-checking. In this article, we'll explore the pitfalls developers often encounter and solutions to navigate the maze of type-guarding

The Quirks of typeof

One might expect the typeof operator to be a reliable means of determining variable types. However, it has its quirks and limitations

The typeof operator is notorious for its inability to distinguish between various data types. For example

console.log(typeof 42);       // Output: "number" ok
console.log(typeof "Hello");  // Output: "string" ok
console.log(typeof true);     // Output: "boolean" ok
console.log(typeof {});       // Output: "object" ok
console.log(typeof []);       // Output: "object" (Wait, what?)
console.log(typeof null);     // Output: "object" (Wait, what again?)
console.log(typeof new String("Hello"));  // Output: "object" (WFT!)
console.log(typeof new Number(42));  // Output: "object" (WFT again!)
Enter fullscreen mode Exit fullscreen mode

The behavior of typeof in JavaScript, influenced by historical design choices and a commitment to backward compatibility, reflects the language's dynamic and flexible nature. Originally designed for simplicity in scripting tasks, typeof exhibits quirks, such as grouping objects and arrays under "object," due to the pragmatic design philosophy prioritizing ease of use over precision. Developers often opt for alternatives like utility-guards for more nuanced type-checking in projects.

Type Tags: Object.prototype.toString

A common workaround using type tags to more accurately determine the type of a variable. For instance:

const getTypeTag = (value) => {
    return Object.prototype.toString.call(value).slice(8, -1);
};

console.log(getTypeTag("Hello"));  // Output: "String"
console.log(getTypeTag(42));       // Output: "Number"
console.log(getTypeTag(true));     // Output: "Boolean"
console.log(getTypeTag({}));       // Output: "Object"
console.log(getTypeTag([]));       // Output: "Array"
console.log(getTypeTag(null));     // Output: "Null"
Enter fullscreen mode Exit fullscreen mode

Here, the getTypeTag function uses Object.prototype.toString to derive a more precise type tag for various values, offering a workaround for the limitations of typeof.

While type tags offer a workaround for more precise type identification, it's essential to acknowledge their limitations, especially in scenarios where objects may be of the same type but have different purposes or structures.

Type tags, although somewhat hackish, provide a pragmatic approach to overcoming the limitations of typeof and constructor behaviors. By leveraging Object.prototype.toString and incorporating custom tags, developers can achieve more accurate type-checking in their JavaScript and TypeScript projects while embracing the pragmatic ethos of the language.

The Patchwork of Type Checks

In the vast landscape of JavaScript, developers grapple with a multitude of type-checking methods, ranging from typeof and Array.isArray to Number.isNaN and the global isNaN. The use of value instanceof Function and typeof value for function type checks further adds to the variety. This diversity, while providing flexibility, often leads to code that is both messy and inconsistent

const myValue = "Hello";

// Using typeof
if (typeof myValue === "string") {
    // Code for string values
}

// Leveraging Array.isArray
if (Array.isArray(myValue)) {
    // Code for arrays
}

// Employing Number.isNaN
if (Number.isNaN(myValue)) {
    // Code for NaN values
}

// Relying on the global isNaN
if (isNaN(myValue)) {
    // Code for non-numeric values
}

// Utilizing instanceof for functions
if (myValue instanceof Function) {
    // Code for function types
}

// Using typeof for function
if (typeof myValue === "function") {
   // Code for function types
}
Enter fullscreen mode Exit fullscreen mode

Streamlining Type-Checking with utility-guards

The inspiration for utility-guards arose from the recurring challenge of inconsistent type-checking methods encountered across various JavaScript projects. Navigating these inconsistencies prompted the creation of a comprehensive library that addresses the diverse needs of type-checking in a unified and unobtrusive manner.

import {
    isString,
    isNumber,
    isBoolean,
    isUndefined,
    isNull,
    isFunction,
    isPrimitive,
    isDate,
    isSymbol,
    isRegExp,
    isError,
    isArray,
    isAnyObject,
    isPlainObject,
    isHas,
    isHasIn,
    isNil,
    isPromise,
    isPromiseLike,
    isIterable,
    isInstanceOf,
    isEmpty,
    isFalsy,
    isArrayOf,
    isNaN,
} from 'utility-guards'
Enter fullscreen mode Exit fullscreen mode

utility-guards provides a unified set of functions covering a broad spectrum of type-checking scenarios. Whether you need to determine if a value is an array, a promise, or an empty object, the library offers a consistent and unobtrusive syntax.

Few words about Lodash

In lodash, you might find utility functions that indirectly help with type-related tasks, such as _.isArray, _.isObject, or _.isFunction.

Lodash is a versatile utility library known for its wide range of functions but doesn't specialize in comprehensive type guards.

The library lacks a complete set of type guards and has static typing limitations.

Summary

Navigating the intricacies of type-checking in JavaScript presents developers with challenges that stem from the language's dynamic and flexible nature. The typeof operator, while a quick go-to for type identification, exhibits quirks that can lead to unexpected outcomes. Type tags, using Object.prototype.toString, offer a pragmatic workaround, though their limitations must be acknowledged. The diverse array of type-checking methods, from Array.isArray to isNaN, often results in code that is inconsistent and difficult to maintain.

In response to these challenges i've created the utility-guards library, designed to assist you in overcoming these issues.

Top comments (4)

Collapse
 
tinkermakar profile image
Makar • Edited

I just use instanceof for most cases

Collapse
 
resetand profile image
Kirill • Edited

No, you can't, for example, a simple string check doesn't work as you may expect:

const value = test
const result = value instanceof String // false
Enter fullscreen mode Exit fullscreen mode

The same with number, boolean, undefined, null, nan. Of course you can use typeof and other things that i mentioned, but the very purpose of the article is that in JS there is no clear mechanism for type validation and to offer an alternative

Collapse
 
tinkermakar profile image
Makar • Edited

Challenge accepted ))

  • for Arrays, Functions and Objects -- {} instanceof Object
  • for numbers (also to exclude NaN) -- Number.isFinite()
  • for strings -- usually not needed, but as a last resort typeof
  • for null & undefined -- usually if (value) or value ?? falbackValue makes the trick, but as a last resort I do a precise check value === undefined
  • for boolean values -- if (value) or if (!!value) is enough, I've never had to check check more precisely
Thread Thread
 
resetand profile image
Kirill

Again, you can. But you just mention several different approaches that solve the same problem, my point is to simplify this