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
filteroperator on the observablechecks if the value is non-nullish with the previously defined function
isNonNullishreturns the source observable
filterNullish - a wrapper function that returns OperatorFunction which is one of the RxJS's operator interfaces. The filtering:
uses native
filteroperatortells the compiler that the returned type is
NonNullish<T>invokes and returns the result of previously defined
isNonNullishfunction
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
inputValuesto test the operatoruse
jest.useFakeTimers()to "control the time"within the test
create observable from test values
from(inputValues)use the operator
filterNullable()inside the pipeaccumulate all the values in the array by using
reduceoperator
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
https://gitlab.com/barcioch-blog-examples/010-null-nullish-nullable




