Skip to content

Latest commit

 

History

History
281 lines (220 loc) · 8.48 KB

README.md

File metadata and controls

281 lines (220 loc) · 8.48 KB

Nullsafe

npm version Build Status Coverage Status

A nice way to do a nullsafe traversal of nested objects.

Supported Node Versions

  • 11
  • 10
  • 9
  • 8
  • 7
  • 6

Motivation

If you have nested objects and want a way to safely traverse it, nullsafe will help you. Let's take an example. Given this object:

const pet = {
  "biggest": {
     "kitty": {
       "name": "Missy"
     }
  }
};

You would normally have to use a few null checks in order to pull the name (Missy) out of there.

let name;
if (pet && pet.biggest && pet.biggest.kitty) {
  name = pet.biggest.kitty.name;
}

Alternatively, you could also use lodash

const _ = require('lodash');
const name = _.get(pet, 'biggest.kitty.name');

But I wanted to be able to pass around optional objects, objects that could either contain a value or not. And I wanted these special optional objects to be safe against anything I invoked against it.

Lastly, I didn't want to care about the differences between null and undefined. This library will treat the two as the same and always return null when the target (thing being wrapped) is either a null or undefined.

Usage

Include the library and wrap your object.

const nullsafe = require('nullsafe');
const person = {
  name: 'Jon'
};
const zip = nullsafe(person)
                    .get('address')  // does not exist
                    .get('zip');     // does not exist

const isNull = zip.isNull();     // true
const value  = zip.value;        // null

Api

Immediately Resolve

If you have an object, and want to immediately evaluate some path, you can. For example:

const nullsafe = require('nullsafe');
const nullableName = nullsafe(pet, ['biggest', 'kitty', 'name']);
const name = nullableName.value;

// or just this
//   const name = nullsafe(pet, ['biggest', 'kitty', 'name']).value;

The advantage of immediately resolving the path is that no intermediate objects are created. In this example above, no proxy objects are created for pet, biggest, or kitty; only the final attribute name is wrapped.

Objects

The most common use case is for passing around objects that could be null.

const nullsafe = require('nullsafe');
const name = nullsafe(pet)
                     .get('biggest')
                     .get('kitty')
                     .get('name').value;

The way it works is simple. The nullsafe method wraps your object in a proxy. You then call the get method to get the attribute you want out. You keep doing this all the way down the chain until you are done. Then you call value in order to unwrap the object. If anywhere in the chain failed, you will get a null back when you unwrap it at the end. Example:

const let = nullsafe(pet)
                    .get('biggest')
                    .get('doggy')
                    .get('name').value;

We don't have a doggy yet, but the traversal will not fail and will return a null at the end. This works no matter how long the chain:

const nameOfMainAttraction = nullsafe(pet)
                                     .get('biggest')
                                     .get('parrot')
                                     .get('name')
                                     .get('otherStuffThatDoesntExist').value;

Functions

nullsafe also supports function calls in the chain.

const kennel = {
  "getSupervisorInfo": () => {
    return {
      "name": "Billy",
      "shift": "Daytime"
    };
  }
};

const supervisorName = nullsafe(kennel).call('getSupervisorInfo').get('name').value;
// Billy

Notice the call method that will invoke the getSupervisorInfo method. Call then wraps the results in a proxy object that we can then chain against. You could of course chain get or call for as long as you want.

We could also pass in parameters:

const mathFunctions = {
  "addTwo": (num) => {
    return num + 2;
  },
  "multiplyTogether": (first, second) => {
    return first * second;
  }
};

const math = nullsafe(mathFunctions);
const sum = math.call('addTwo', 5).value;  // 7
const product = math.call('multiplyTogether', 10, 4).value;  // 40

Or if you prefer apply over call, which is basically the same thing except that the arguments are all passed in a single array:

const mathFunctions = {
  "addTwo": (num) => {
    return num + 2;
  },
  "multiplyTogether": (first, second) => {
    return first * second;
  }
};

const math = nullsafe(mathFunctions);
const sum = math.apply('addTwo', [5]).value;  // 7
const product = math.apply('multiplyTogether', [10, 4]).value;  // 40

Arrays

Array dereferencing is also supported.

const school = {
  "students": [
    {
      "name": "Jane"
    },
    {
      "name": "Robin"
    }
  ]
};

const firstStudentName = nullsafe(school).get('students', 0).get('name').value;  // Jane
const bogusStudentName = nullsafe(school).get('students', 100).get('name').value;  // null

To access elements in an array, you just pass as the second argument to get the array index that you want to access.

Unnested Objects

You could also use it to null check non-nested objects as well.

const nullThing = null;
const someFunction = () => "hi";
const someArray = [100, 200];

const greeting = nullsafe(someFunction).call().value;  // hi
const num = nullsafe(someArray).get(1).value;  // 200

const nullFunction = nullsafe(nullThing).call().value;  // null
const nullArrayAccess = nullsafe(nullThing).get(2).value;  // null
const nullObjectAccess = nullsafe(nullThing).get('id').value;  // null

Raw Value

In the rare cases you care if the value that is wrapped is an actual undefined vs null value, you can get that by calling rawValue.

// rawValue
const undefinedThing = nullsafe(undefined).rawValue; // undefined
const nullThing = nullsafe(null).rawValue; // null
const name = nullsafe('jon').rawValue; // jon

// value
const undefinedThing2 = nullsafe(undefined).value; // null
const nullThing2 = nullsafe(null).value; // null
const name2 = nullsafe('jon').value; // jon

Performance

Benchmarks performed using Benchmark.js. Benchmark code can be found here.

Run on:

  • MacBook Pro (15-inch, 2016)
  • macOS High Sierra 10.13.4 (17E202)
  • Processor 2.6 GHz Intel Core i7
  • Memory 16 GB 2133 MHz LPDDR3

Comparing:

  1. Lodash v4.17.11
  2. Ramda v0.26.1
  3. Nullsafe v2.0.2

Run 1

Lodash#get x 3,352,635 ops/sec ±3.80% (79 runs sampled)
Ramda#path x 16,985,122 ops/sec ±2.32% (84 runs sampled)
Nullsafe#value x 17,435,296 ops/sec ±1.96% (76 runs sampled)
Nullsafe#rawValue x 17,489,960 ops/sec ±1.37% (88 runs sampled)
Fastest is Nullsafe#rawValue,Nullsafe#value,Ramda#path

Run 2

Lodash#get x 3,807,708 ops/sec ±2.22% (84 runs sampled)
Ramda#path x 18,124,155 ops/sec ±1.67% (89 runs sampled)
Nullsafe#value x 17,382,109 ops/sec ±1.51% (83 runs sampled)
Nullsafe#rawValue x 17,557,116 ops/sec ±1.26% (86 runs sampled)
Fastest is Ramda#path

Run 3

Lodash#get x 3,856,657 ops/sec ±1.90% (86 runs sampled)
Ramda#path x 17,466,883 ops/sec ±2.48% (86 runs sampled)
Nullsafe#value x 17,630,377 ops/sec ±1.03% (89 runs sampled)
Nullsafe#rawValue x 17,630,998 ops/sec ±1.07% (89 runs sampled)
Fastest is Nullsafe#value,Nullsafe#rawValue,Ramda#path

Run 4

Lodash#get x 3,864,329 ops/sec ±2.37% (77 runs sampled)
Ramda#path x 18,743,263 ops/sec ±1.83% (80 runs sampled)
Nullsafe#value x 17,284,128 ops/sec ±1.59% (86 runs sampled)
Nullsafe#rawValue x 17,538,949 ops/sec ±1.36% (89 runs sampled)
Fastest is Ramda#path

Run 5

Lodash#get x 4,136,289 ops/sec ±1.94% (81 runs sampled)
Ramda#path x 18,449,161 ops/sec ±1.07% (86 runs sampled)
Nullsafe#value x 17,517,517 ops/sec ±1.33% (84 runs sampled)
Nullsafe#rawValue x 17,648,284 ops/sec ±1.35% (87 runs sampled)
Fastest is Ramda#path