Home > Software design >  Is it possible to design a type guard for an array being empty?
Is it possible to design a type guard for an array being empty?

Time:08-19

There are numerous examples of how to design a type guard for an array being NOT empty. For example this method works great 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() // does not error
else 
 foo[0].toString() // does error

However to check the inverse of this one must invert the boolean check:

let foo = [1, 2, 3];

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

The problem is I think if (!isNotEmpty(foo)) is a bit ugly to read because it is a double negative.

So the question is, how to define an isEmpty type guard so that one can do if (isEmpty(foo)) and still get the same result as the code snippet shown above? It seems like a trivial problem but all my attempts so far have been thwarted.

I think the main issue is that you cannot assert the inverse of a type guard, you cannot say that something IS NOT something else.

EDIT: I have been asked to provide more examples.

So here is an example of what I would like to do:

function logFirstDataElement(data: number[]) {
  // Dont do anything if no data
  if (isEmpty(data)) return;

  // this should not error because data should have been narrowed to 
  // [T, ...T] 
  // that is, it should have at least one element
  console.log(data[0].toString()) 
}

It can be achieved by doing the following

function logFirstDataElement(data: number[]) {
  // Dont do anything if no data
  if (!isNotEmpty(data)) return;
  console.log(data[0].toString()) 
}

But as mentioned above I would rather avoid the "double negative" of !isNotEmpty(data)

CodePudding user response:

You can use a predicate that sets the array to the empty tuple []:

TS Playground

The playground (and code below) uses the compiler option noUncheckedIndexedAccess (just like in your question), so even in the false branch, you'll have to validate each indexed element to get 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
}

Update in response to the updated question: This type guard has a positive name, so using the logical NOT (!) to invert it won't result in a double negative:

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

  • Related