NonEmptyArray type

Oluwafemi Shobande
Typescript Tidbits
Published in
3 min readMar 16, 2024

Whenever you access a specific index in an array, there’s a possibility that the result is undefined.

const meetings: string[] = [];
const firstMeeting = meetings[0]; // undefined

Before the introduction of the noUncheckedIndexAccess TS config in Typescript 4.1, although firstMeeting is possibly undefined, its inferred type is string not string | undefined. However, now that we have this new config, enabling it will cause firstMeeting to be string | undefined.

One consequence of enabling noUncheckedIndexAccess is always having to check that the element at some index is not undefined before using it. In my opinion, this is great, because I’d rather have the compiler force me to explicitly check for undefined than encounter some runtime errors at a later time. For the rest of this post, we’ll assume that this config is enabled.

Admittedly, this can be stressful especially when you know for sure that an element exists at that index. More often than not, the next course of action is using the dreaded non-null assertion (!).

My rule of thumb is that whenever I see a non-null assertion, the type definitions can be improved to eliminate this assertion.

In this post, I’ll show you how to avoid non-null assertions when accessing the first element of an array when you know for sure that it is not undefined.

NonEmptyArray

We can make use of the open-ended property of tuple types. This is accomplished by using Rest elements.

type NonEmptyArray<T> = [T, ...T[]];

In essence, `NonEmptyArray` is a tuple where the first element is of type T and there is a possibility of zero or more additional elements of type T.

Example Usage 1

We can enforce at compile time that a function accepts an array having at least one element.

Instead of:

function sumValues(value: number[]) {
}

sumValues([]); //✅︎ this should not be allowed but it is
sumValues([1]); //✅︎
sumValues([1, 4, 5, 6]); //✅︎

We may do this

function sumValues(value: NonEmptyArray<number>) { // non empty array expected
}

sumValues([]); //❌ no longer allowed
sumValues([1]); //✅︎
sumValues([1, 4, 5, 6]); //✅︎

Example Usage 2

I’ve found the NonEmpty array type to be useful when grouping an array of elements by keys. Here’s a generic groupBy function.

function groupBy<Item, Key>(
items: Item[],
getKey: (item: Item) => Key
): Map<Key, Item[]> {
const itemsGroupedByKey = new Map<Key, Item[]>();
for (const item of items) {
const key = getKey(item);
const itemGroup = itemsGroupedByKey.get(key);
if (itemGroup === undefined) {
itemsGroupedByKey.set(key, [item]);
} else {
itemGroup.push(item);
}
}
return itemsGroupedByKey;
}

Suppose we have an array of cars and we group them by manufacturer, when fetching the list of cars belonging to a specific manufacturer, if the manufacturer’s key exists in the map, we are certain that its corresponding value is an array of one or more cars.

interface Car {
manufacturer: string;
model: string;
price: number;
}

const cars: Car[] = [
{manufacturer: "Toyota", model: "Corolla", price: 10_000},
{manufacturer: "Toyota", model: "Camry", price: 15_000},
{manufacturer: "Kia", model: "Sportage", price: 18_000},
{manufacturer: "Hyundai", model: "Tucson", price: 16_000},
];

const carsByManufacturer = groupBy(cars, (car) => car.manufacturer);
const toyotaCars = carsByManufacturer.get("Toyota");
if (toyotaCars !== undefined) {
const firstToyotaCar = toyotaCars[0]; // inferred type is 'Car | undefined'
serviceCar(firstToyotaCar); //❌ 'Car|undefined' cannot be assigned to 'Car'
}

function serviceCar(car: Car) {
// ...
}

Although we’re 100% sure that toyotaCars has at least 1 element, when accessing its first index, its type is still inferred as Car | undefined. We could make a non-null assertion, but remember we’re trying to avoid this.

Here’s an improvement to the groupBy function using NonEmptyArray:

function groupBy<Item, Key>(
items: Item[],
getKey: (item: Item) => Key
): Map<Key, NonEmptyArray<Item>> { // value type is NonEmptyArray<Item>
const itemsGroupedByKey = new Map<Key, NonEmptyArray<Item>>();
for (const item of items) {
const key = getKey(item);
const itemGroup = itemsGroupedByKey.get(key);
if (itemGroup === undefined) {
itemsGroupedByKey.set(key, [item]);
} else {
itemGroup.push(item);
}
}
return itemsGroupedByKey;
}

const carsByManufacturer = groupBy(cars, (car) => car.manufacturer);
const toyotaCars = carsByManufacturer.get("Toyota");
if (toyotaCars !== undefined) {
const firstToyotaCar = toyotaCars[0]; // inferred type is Car
serviceCar(firstToyotaCar); //✅︎
}

function serviceCar(car: Car) {
// ...
}

If you like this content, kindly subscribe to this publication as I will be sharing many more neat tricks. Also here’s my Twitter, if you’d like to reach out.

--

--