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 []
:
The playground (and code below) uses the compiler option
noUncheckedIndexedAccess
(just like in your question), so even in thefalse
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:
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());
}