Typescript: How to create a non empty array Type

Typescript: How to create a non empty array Type

Typescript is a powerful and expressive language, which allows you to avoid publishing bugs.

Remember, Typescript is not just a "super set" over Javascript, it is a "Turing complete" language in its type system, that is, you can implement complex algorithms using only types.

But its power lies in how well you express the constraints of your application, that is, how well you define your types.

Typescript forces you to think differently when implementing a solution. You must think about how data flows through your program and how this information is transformed from "one form to another" That is, you must think about what types you will use.

One data type we use constantly is Array.

An array allows you to work with collections of data simply and efficiently.

In both Javascript and Typescript an array can be initialized just by using [] and you can define its type in a similar way or by using the generic form Array<T>

// An array that can contain any tipe and will be declared empty
const arr: unknown[] = []

// Another way to define the type
const arr2: Array<unknown> = []

// An array of strings defined with one element
const arr3: string[] = ['str']

How to avoid using empty arrays with Typescript?

An array can then be empty or contain n elements.

It is a common task to check whether or not an array is empty to operate on it. How can you determine if an array is empty?

In Javascript, this task is done with a conditional block and checking the .length property of the array.

But is it possible to use Typescript's typing language to prevent an array from being empty without using a conditional?

The idea here is to let Typescript check the data stream and give us an error if you're trying to access an empty array.

What you will do is create a new type similar to Array that allows you to define an array that cannot be empty by definition.

Let's call this type NonEmptyArray.

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

const emptyArr: NonEmptyArray<Item> = [] // error ❌

const emptyArr2: Array<Item> = [] // ok ✅ 

function expectNonEmptyArray(arr: NonEmptyArray<unknown>) {
    console.log('non empty array', arr)
}

expectNonEmptyArray([]) // you cannot pass an empty array. ❌

expectNonEmptyArray(['som valuue']) // ok ✅

So whenever you require, for example, a function parameter to be an array that cannot be empty, you can use NonEmptyArray .

The only drawback is that you will now require a "type guard" function since simply checking if the length property of an array is not 0 will not transform it to type NonEmptyArray

function getArr(arr: NonEmptyArray<string>) {
  return arr;
}

const arr3 = ['1']
if (arr3.length > 0)) {
  // ⛔️ Error: Argument of type 'string[]' is not
  // assignable to parameter of type 'NonEmptyArr<string>'.
  getArr(arr3);
}

This error occurs because getArr expects the argument to be NonEmptyArray, but arr3 is of type Array.

Type guards

A "type-guard" function allows you to "help" Typescript correctly infer the type of some variable.

It is a simple function that returns a boolean value. If this value is true then Typescript will consider the variable evaluated to be one type or another.

// Type Guard
function isNonEmpty<A>(arr: Array<A>): arr is NonEmptyArray<A>{
    return arr.length > 0
}

This function receives a generic array (hence the use of A), and checks to see if the length property is greater than 0.

This function is marked to return arr is NonEmptyArray<A> i.e. that the value of the condition evaluated is true Typescript will understand that the parameter to use arr is of type NonEmptyArray

// Type Guard
function isNonEmpty<A>(arr: Array<A>): arr is NonEmptyArray<A>{
    return arr.length > 0
}

function getArr(arr: NonEmptyArray<string>) {
  return arr;
}

const arr3 = ['1']
//     ^?   const arr3: string[]
if (isNonEmpty(arr3)) {
  getArr(arr3);
//        ^? const arr3: NonEmptyArray<string>
}

A simple way to understand type guard is that you "cast" one type to another type if and only if a certain condition is met. What makes this transformation safe compared to a simple type cast as NonEmptyArray

Check out the typescript playground with these examples.