3

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)

mikeysee
  • 1,613
  • 1
  • 18
  • 31
  • Re: Your [update](https://stackoverflow.com/revisions/73398077/3): asserting to the compiler that an array is not empty does not assert that it has a value at index `0`. Those are different assertions and require different type guards. (e.g. an array can not be empty, but the first 200 elements are `undefined`, with the first not-undefined element appearing only at index 200.) – jsejcksn Aug 18 '22 at 07:14
  • Maybe it worth to use `const isFilled=(arr:any[]): arr is [any, ...any[]]=> your code`. Just use two typeguards – captain-yossarian from Ukraine Aug 18 '22 at 07:16
  • @jsejcksn is that true tho? If you declare your array as `number[]` then arent you asserting that is a contiguous array with ONLY numbers in it? – mikeysee Aug 18 '22 at 07:20
  • @jsejcksn if that is the case then perhaps rather than `isEmpty` I need an `doesNotHaveAFirstElement` or something? – mikeysee Aug 18 '22 at 07:21
  • @captain-yossarianfromUkraine if would like to avoid having two type guards in my if-statement if possible – mikeysee Aug 18 '22 at 07:23
  • [^](https://stackoverflow.com/questions/73398077/is-it-possible-to-design-a-type-guard-for-an-array-being-empty#comment129620934_73398077) @mikeysee Yes, that is true: https://tsplay.dev/wRl9nm – jsejcksn Aug 18 '22 at 07:24
  • @mikeysee See my updated answer. It addresses your clarified concern about using a "double negative". – jsejcksn Aug 18 '22 at 07:27

1 Answers1

2

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

jsejcksn
  • 27,667
  • 4
  • 38
  • 62
  • No this is not correct because the non-empty case array value should not be `number | undefined` it should be `number` because we have just asserted that it is NOT empty – mikeysee Aug 18 '22 at 06:41
  • [^](https://stackoverflow.com/questions/73398077/is-it-possible-to-design-a-type-guard-for-an-array-being-empty/73398433#comment129620215_73398433) @mikeysee No, that's not how [`noUncheckedIndexedAccess`](https://www.typescriptlang.org/tsconfig#noUncheckedIndexedAccess) works. If you want the behavior that you described, you'll have to disable that compiler option. Here's a link to the same playground with that option disabled: https://tsplay.dev/Nr4v0W – jsejcksn Aug 18 '22 at 06:43
  • Well you are able to do the inverse of that as I mentioned in my OP. The type remaining after the isEmpty check should be a tuple with at least one element `[T, ...T]` – mikeysee Aug 18 '22 at 06:47
  • @mikeysee It will be if the input array is such a type (e.g. `const array: [number, ...number[]] = [1, 2, 3];`). – jsejcksn Aug 18 '22 at 06:50
  • So its not possible to do it without declaring that ugly tuple type on the "array" variable? Seems like it should be possible to simply turn `!isNotEmpty()` into a type guard but I guess not? – mikeysee Aug 18 '22 at 06:53
  • @mikeysee That's right. [Type guards](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates) are for narrowing to a single type (not multiple types). Tuples aren't ugly! They (IMO, very elegantly) express that a list always has certain type members at certain indexes. – jsejcksn Aug 18 '22 at 06:55
  • Okay well if you would like to add another answer stating that its not possible then I would be happy to mark that one as correct. – mikeysee Aug 18 '22 at 06:59
  • @mikeysee Well, the question that you asked was "Is it possible to design a type guard for an array being empty?", and that's what this answer is in response to. – jsejcksn Aug 18 '22 at 07:00
  • @mikeysee try to use `array is never[]` instead of `array is []`. TS infers empty array as `never[]`. Hence if you try to get `array[0]` from empty array you will get `undefined` and TS will complain if you try to call `toString`. It all depends on what type of error you want to get. Please provide more examples when you want to get an error from TS – captain-yossarian from Ukraine Aug 18 '22 at 07:03
  • Okay I see, well its more nuanced than I could fit in the title. I wanted the narrowed type in the negative case to be a non-empty array as I mentioned in my OP: "and still get the same result as the code snippet shown above?" – mikeysee Aug 18 '22 at 07:03
  • @captain-yossarianfromUkraine I have updated the OP with another example to better explain what I am trying to do. – mikeysee Aug 18 '22 at 07:09
  • @jsejcksn okay thanks for your update. Its not exactly the result I was hoping for but I guess its as close as im going to get :) I appreciate all the time you have spent looking into this and being patient with me. – mikeysee Aug 18 '22 at 07:29
  • @mikeysee Glad it works. Maybe a useful read to you: [the beauty of code is the concepts expressed within, not the code itself](https://blog.codinghorror.com/code-isnt-beautiful/). – jsejcksn Aug 18 '22 at 08:00