Can a custom type guard be created to check if an array is empty?

There are various methods for creating a type guard to ensure that an array is not empty. An example of this can be found here, which works well when using noUncheckedIndexedAccess:

type Indices<L extends number, T extends number[] = []> = T["length"] extends L
  ? T[number]
  : Indices<L, [T["length"], ...T]>;

export type LengthAtLeast<T extends readonly any[], L extends number> = Pick<
  Required<T>,
  Indices<L>
>;

// Borrowed from: https://stackoverflow.com/a/69370003/521097
export function hasLengthAtLeast<T extends readonly any[], L extends number>(
  arr: T,
  len: L
): arr is T & LengthAtLeast<T, L> {
  return arr.length >= len;
}

export function isNotEmpty<T extends readonly any[]>(arr: T): arr is T & LengthAtLeast<T, 1> {
  return hasLengthAtLeast(arr, 1);
}

then:

let foo = [1, 2, 3];

if (isNotEmpty(foo)) 
  foo[0].toString() // no error
else 
 foo[0].toString() // error

However, to check if the array is empty, you have to negate the boolean condition:

let foo = [1, 2, 3];

if (!isNotEmpty(foo)) 
  foo[0].toString(); // now errors
else 
  foo[0].toString(); // no error

A potential issue arises with if (!isNotEmpty(foo)) as it involves a double negative making it hard to read.

So the question remains, how can we define an isEmpty type guard so that we can simply do if (isEmpty(foo)) and achieve the same outcome as shown above? It seems like a simple problem, but my attempts have been unsuccessful so far.

I believe the main challenge lies in asserting the inverse of a type guard, indicating something IS NOT another thing.

EDIT: More examples have been requested.

Here is an example of what I aim to achieve:

function logFirstDataElement(data: number[]) {
  // Do nothing if no data present
  if (isEmpty(data)) return;

  // This should not result in an error because data should be narrowed down to
  // [T, ...T]
  // ensuring it has at least one element
  console.log(data[0].toString())
}

This can be accomplished by:

function logFirstDataElement(data: number[]) {
  // Do nothing if no data present
  if (!isNotEmpty(data)) return;
  console.log(data[0].toString())
}

As mentioned earlier, I would prefer to avoid the "double negative" confusion of !isNotEmpty(data).

Answer №1

A way to set the array to an empty tuple [] is by using a predicate:

TS Playground

This code snippet in the playground uses the compiler option noUncheckedIndexedAccess (similar to your question), requiring validation of each indexed element even in the false branch to obtain a non-nullable value.

function isEmpty (array: readonly any[]): array is [] {
  return array.length === 0;
}

const array = [1, 2, 3];

if (isEmpty(array)) {
  const value = array[0]; /*
                      ~
  Tuple type '[]' of length '0' has no element at index '0'.(2493) */
}
else {
  const value = array[0];
      //^? const value: number | undefined
}

In response to the revised query: The positive name of this type guard eliminates double negation when using the logical NOT (!) to invert it:

TS Playground

function hasAtLeastOneElement <T>(array: readonly T[]): array is [T, ...T[]] {
  return array.length > 0;
}

function logFirstElement (array: number[]) {
  if (!hasAtLeastOneElement(array)) {
    const [first] = array;
         //^? const first: number | undefined
    return;
  }

  const [first] = array;
       //^? const first: number

  console.log(first.toString());
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

The type FormGroup<any> lacks the controls and registerControl properties compared to AbstractControl<any>

I've been developing a reactive nested form inspired by this YouTube tutorial - https://www.youtube.com/watch?v=DEuTcG8DxUI Overall, everything is working fine, except for the following error - Below are my files. home.component.ts import { Compone ...

Having difficulty properly streaming UI components with Vercel's AI-SDK

Recently, I've been diving into the new Vercel's AI-SDK to expand my skills. My current project involves developing a persona generator that takes specific guidelines as inputs and generates persona attributes according to a given Zod schema. B ...

Unveiling individual modules of an Angular library using public-api.ts in the latest version of Angular (Angular 13)

After completing an upgrade on my Angular library project from version 11 to 13, I encountered an issue when attempting to execute the ng build command. In version 11, the setup looked like this: I had multiple smaller modules, each containing various co ...

Troubleshooting: Why are my Angular 8 Carousel Slide Animations not functioning

Looking to create a carousel slideshow with images sliding from right to left and smoothly transition to the next image. All the necessary code can be found in this STACKBLITZ Here is the HTML snippet: <ngb-carousel *ngIf="images" [showNavigationArro ...

What is the process for removing a document with the 'where' function in Angular Fire?

var doc = this.afs.collection('/documents', ref => ref.where('docID', '==', docID)); After successfully retrieving the document requested by the user with the code above, I am unsure of how to proceed with deleting that do ...

Using `await` inside an if block does not change the type of this expression

Within my code, I have an array containing different user names. My goal is to loop through each name, verify if the user exists in the database, and then create the user if necessary. However, my linter keeps flagging a message stating 'await' h ...

Guide on combining two classes as the base class for an extended class in Typescript

Is it possible to create a higher order function (HOF) that modifies or adds a property to the prototype of a given class? interface IStore { new (): {}; } interface IWatchable { new() : { watch: boolean; }; } const Store = <T extends ISt ...

Encountered an error trying to access '0' property of an undefined object when iterating through data in angular framework

My API is returning data in the format shown below: "fileName": "data.txt", "onlyInFile1": [ { "_id": "60618e87c2077428e4fedde5", "TERMINAL_ID": "Y6152114", "EXTERNAL_STAN": & ...

How to assign a new type to a class in Typescript

I am attempting to re-export a class with an internal type declaration in Typescript. My goal is for the re-exported class to be usable both as a class (with new) and as a type. Below is an example of what I have tried: class XReal { foo() {return 5} } dec ...

Setting up Mailgun with TypeScript on Firebase Cloud Functions

Currently, I am working on a Cloud Function within Firebase to integrate with Mailgun for sending emails, following the guidelines provided in the Mailgun documentation. My challenge lies in implementing this functionality using TypeScript, as I have been ...

"Creating a Typescript function that guarantees a non-null and non-undefined return

Currently, I am working on developing a function that is designed to return a specific value. In the event that the returned value is null or undefined, the function should then default to a pre-determined value. function test<A, B>(input: A, fallba ...

The RouteParams encounter a problem because it is unable to resolve all parameters

I'm encountering an issue with the RC3 router. The error message I am receiving is: Can't resolve all parameters for RouteParams: (?). Below is my code: //route.ts import {provideRouter, RouterConfig} from '@angular/router'; import {H ...

Automatic type inference for functions in TypeScript with arguments

I am looking to define an interface with the following structure: interface CheckNActSetup<D, C> { defs: (event: Event) => D, context: (defs: D) => C; exec: (context: C) => any[]; when: ((context: C) => boolean)[]; } and implement it usi ...

The name is not found when using attribute, but it is found when using extends

Lately, I've encountered difficulties with creating large TypeScript modules, and there's one thing that has been puzzling me. Specifically, the following scenario doesn't seem to work: // file A.ts export = class A { } // file main.ts imp ...

Angular: failure to update a specific portion of the view

I'm currently working on a directive template that features the following code snippet: <div class="colorpicker"> <div>Chosen color</div> <div class="color_swatch" style="background-color: {{ngModel}}">&nbsp;</div> & ...

Angular is notifying that an unused expression was found where it was expecting an assignment or function call

Currently, I am working on creating a registration form in Angular. My goal is to verify if the User's username exists and then assign that value to the object if it is not null. loadData(data: User) { data.username && (this.registrationD ...

Receiving feedback from an http.post request and transferring it to the component.ts file in an Angular

Currently, I am facing an issue with passing the response from an http.post call in my TypeScript service component to an Angular 2 component for display on the frontend. Below are the code structures of my service.ts and component.ts: getSearchProfileRes ...

Maximizing the efficiency of enums in a React TypeScript application

In my React application, I have a boolean called 'isValid' set like this: const isValid = response.headers.get('Content-Type')?.includes('application/json'); To enhance it, I would like to introduce some enums: export enum Re ...

How can the outcome of the useQuery be integrated with the defaultValues in the useForm function?

Hey there amazing developers! I need some help with a query. When using useQuery, the imported values can be undefined which makes it tricky to apply them as defaultValues. Does anyone have a good solution for this? Maybe something like this would work. ...

Can [] be considered a valid type in Typescript language?

I've come across this function: function stringToArray(s: string|[]): [] { return typeof s === 'string' ? JSON.parse(s.replace(/'/g, '"')) : s; } This function is functioning as expected without any type warnings. Bu ...