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 observablechecks 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
operatortells 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 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
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();
});
});