Null, nullish, nullable

Null, nullish, nullable

In most programming languages there is only one way of defining whether the value is set or not. That value is generally considered null but named differently. But the Javascript went one step further and introduced two of them: null and undefined. Usually, they are considered in the same fashion - both are treated as nullable values which means there is no difference (for the developer) which of them is assigned. In this article, I'll show how to make your life easier by introducing functions, Angular pipes and RxJS operator to handle them.

Setup

Requirements: Node, npm, npx

Install dependencies

npm i jest@29 @types/jest@29 ts-jest rxjs typescript

Basic null checks

The most basic null check can be done in a few ways.

The first one is a loose comparison. Comparing a nullable value directly to the null or undefined results in true.

if (value == null) {
  // true if value is null or undefined
}

if (value == undefined) {
  // true if value is null or undefined
}

When enforcing strict equality the code becomes more tedious.

if (value === null || value === undefined) {
  // true if value is null or undefined
}

Types and helpers

Let's create the types and helper functions.

Nullable<T> - a union of current variable type, null and undefined.

export type Nullable<T> = T | null | undefined;

Nullish - a union of null and undefined types.

export type Nullish = null | undefined;

NonNullish<T> - a current variable type with null and undefined types excluded. The Exclude comes from standard Typescript's utility types. You can also use the Typescript's NonNullable<T> type but I prefer my definition for more readability.

export type NonNullish<T> = Exclude<T, null | undefined>;

isNullish function - checks whether the value is null or undefined.

export const isNullish = (value: unknown): value is Nullish => value === null || value === undefined;

isNonNullish function - checks whether the value is different from null and undefined.

export const isNonNullish = (value: unknown): value is NonNullish<unknown> => value !== null && value !== undefined;

The file with definitions might look like this

// nullable.ts

export type Nullable<T> = T | null | undefined;
export type Nullish = null | undefined;
export type NonNullish<T> = Exclude<T, null | undefined>;

export const isNullish = (value: unknown): value is Nullish => value === null || value === undefined;
export const isNonNullish = (value: unknown): value is NonNullish<unknown> => value !== null && value !== undefined;

Test it

Prepare the datasets, pass the values to the functions and check the result.

// nullable.spec.ts
import { isNonNullish, isNullish } from './nullable';

const isNullishDataset: { key: string; value: any; expectedResult: boolean }[] = [
  { key: 'null', value: null, expectedResult: true },
  { key: 'undefined', value: undefined, expectedResult: true },
  { key: 'string290', value: 'string290', expectedResult: false },
  { key: '""', value: '', expectedResult: false },
  { key: 'false', value: false, expectedResult: false },
  { key: 'true', value: true, expectedResult: false },
  { key: '1', value: 1, expectedResult: false },
  { key: '-1', value: -1, expectedResult: false },
  { key: '0', value: 0, expectedResult: false },
  { key: 'Infinity', value: Infinity, expectedResult: false },
  { key: '-Infinity', value: -Infinity, expectedResult: false },
  { key: '[]', value: [], expectedResult: false },
  { key: '["e1", 23]', value: ['e1', 23], expectedResult: false },
  { key: '{}', value: {}, expectedResult: false },
  { key: '{prop1: false, prop2: 90}', value: { prop1: false, prop2: 90 }, expectedResult: false },
  {
    key: '() => {}', value: () => {
    }, expectedResult: false
  },
  { key: 'NaN', value: NaN, expectedResult: false },
  { key: 'new Error()', value: new Error(), expectedResult: false },
];


describe('Test isNullish', () => {
  it.each(isNullishDataset)('value: $key', ({ value, expectedResult }) => {
    expect(isNullish(value)).toEqual(expectedResult);
  });
});

const isNonNullishDataset: { key: string; value: any; expectedResult: boolean }[] = [
  { key: 'null', value: null, expectedResult: false },
  { key: 'undefined', value: undefined, expectedResult: false },
  { key: 'string290', value: 'string290', expectedResult: true },
  { key: '""', value: '', expectedResult: true },
  { key: 'false', value: false, expectedResult: true },
  { key: 'true', value: true, expectedResult: true },
  { key: '1', value: 1, expectedResult: true },
  { key: '-1', value: -1, expectedResult: true },
  { key: '0', value: 0, expectedResult: true },
  { key: 'Infinity', value: Infinity, expectedResult: true },
  { key: '-Infinity', value: -Infinity, expectedResult: true },
  { key: '[]', value: [], expectedResult: true },
  { key: '["e1", 23]', value: ['e1', 23], expectedResult: true },
  { key: '{}', value: {}, expectedResult: true },
  { key: '{prop1: false, prop2: 90}', value: { prop1: false, prop2: 90 }, expectedResult: true },
  {
    key: '() => {}', value: () => {
    }, expectedResult: true
  },
  { key: 'NaN', value: NaN, expectedResult: true },
  { key: 'new Error()', value: new Error(), expectedResult: true },
];

describe('Test isNonNullish', () => {
  it.each(isNonNullishDataset)('value: $key', ({ value, expectedResult }) => {
    expect(isNonNullish(value)).toEqual(expectedResult);
  });
});

RxJS operator to filter nullish values

The operator

  • invokes native filter operator on the observable

  • checks if the value is non-nullish with the previously defined function isNonNullish

  • returns the source observable

filterNullish - a wrapper function that returns OperatorFunction which is one of the RxJS's operator interfaces. The filtering:

  • uses native filter operator

  • tells the compiler that the returned type is NonNullish<T>

  • invokes and returns the result of previously defined isNonNullish function

import { filter, OperatorFunction } from 'rxjs';
import { isNonNullish, NonNullish } from './nullable';

export function filterNullable<T>(): OperatorFunction<T, NonNullish<T>> {
  return filter((value: T): value is NonNullish<T> => isNonNullish(value));
}

Test it

The test is more complex

  • define inputValues to test the operator

  • use jest.useFakeTimers() to "control the time"

  • within the test

    • create observable from test values from(inputValues)

    • use the operator filterNullable() inside the pipe

    • accumulate all the values in the array by using reduce operator

    reduce((acc, val) => {
      acc.push(val);
    
      return acc;
    }, [] as any[])
    
    • subscribe to the result and run the expect
    subscribe(result => {
      expect(result).toEqual(expectedResult);
    })
    
    • advance timers jest.advanceTimersToNextTimer() allowing observable to be processed

The whole test

// filter-nullish-operator.spec.ts

import { from, reduce } from 'rxjs';
import { filterNullish } from './filter-nullish-operator';

const inputValues = [
  null,
  undefined,
  'string290',
  '',
  false,
  true,
  1,
  -1,
  0,
  Infinity,
  -Infinity,
  [],
  [
    'e1',
    23,
  ],
  {},
  {
    'prop1': false,
    'prop2': 90,
  },
  NaN,
];

const expectedResult = [
  'string290',
  '',
  false,
  true,
  1,
  -1,
  0,
  Infinity,
  -Infinity,
  [],
  [
    'e1',
    23,
  ],
  {},
  {
    'prop1': false,
    'prop2': 90,
  },
  NaN,
];

jest.useFakeTimers();

describe('Test filterNullish operator', () => {
  it('should filter nullish values', () => {
    from(inputValues).pipe(
      filterNullish(),
      reduce((acc, val) => {
        acc.push(val);

        return acc;
      }, [] as any[]),
    ).subscribe(result => {
      expect(result).toEqual(expectedResult);
    })

    jest.advanceTimersToNextTimer();
  });
});

Source code

gitlab.com/barcioch-blog-examples/010-null-..