2

I was wondering if it's possible to create a type guard which checks if every item of an array is defined. I already have a type guard for checking a single value, but having a solution that would do that for a whole array (with would be great.

To put it in code, I'm looking for something that would type guard the following:

// Input array type:
[ Something | undefined, SomethingDifferent | undefined ]

// Type guard resolving to:
[ Something, SomethingDifferent ]

Currently I have the following type guard to check a single item:

function isDefined<T>(value: T | undefined | null): value is T {
    return value !== null && typeof value !== 'undefined';
}

The logical implementation of the type guard is quite easy (especially when reusing the guard for a single value):

function isEachItemDefined<T extends Array<unknown>>(value: T): ?? {
    return value.every(isDefined);
}

My use case for this would be filtering after using the withLatestFrom operator (RxJS). I have an edge case where I'd like to filter out the "event" whenever any of the values is not defined.

observable1.pipe(
    withLatestFrom(observable2, observable3, observable4), // <-- each returns value or undefined
    filter(isEachItemDefined),
    map(([ item1, item2, item3, item4 ]) => {
      // event if the logic works correctly,
      // typescript wouldn't recognise the items as defined
      // e.g. item2 is still "string | undefined"
    }),
);
Dawid Zbiński
  • 5,521
  • 8
  • 43
  • 70
  • side question: why `typeof value !== 'undefined'` instead of just `value !== undefined` – Inigo Dec 11 '21 at 23:13
  • @Inigo Good question actually, if the variable is not specified in the code at all, it would throw the ReferenceError without typeof. Of course, in my example it's not logical at all, as the same would happen with the null check I have there. After all we have good IDEs nowadays and this should not happen at all :) Good point. – Dawid Zbiński Dec 11 '21 at 23:25

1 Answers1

1
function isDefined<T> (value: NonNullable<T> | undefined | null): value is NonNullable<T> {
    return value !== null && value !== undefined
}

function isEachItemDefined<T> (value: Array<NonNullable<T> | undefined | null>): value is Array<NonNullable<T>> {
    return value.every(isDefined)
}

Here's some code to prove it works:

function isEasyGoing<T>(arr: (T | undefined | null)[]) {
    // do stuff
}

function isHighMaintenance<T>(arr: NonNullable<T>[]) {
    // do stuff carefully
}

function handle<T>(arr: (NonNullable<T> | undefined | null)[]) {
    if (isEachItemDefined(arr)) {
        isHighMaintenance(arr)  // narrowed to string[], no type check error
    } else {
        isEasyGoing(arr) // still (string | undefined | null)[]
    }
}

const withoutNull: string[] = ['a', 'b', 'c']
const withNull: (string | undefined | null)[] = ['a', 'b', null, 'c']

isHighMaintenance(withoutNull)
isHighMaintenance(withNull)    // type check error as expected

handle(withoutNull)
handle(withNull)

type NullableString = string | null
const nullableWithoutNull: (NullableString | undefined | null)[] = ['a', 'b', 'c']
const nullableWithNull: (NullableString | undefined | null)[] = ['a', 'b', null, 'c']

// Since the generic parameter is constrained as NonNullable<T>
// we can't sneak NullableString by as T
isHighMaintenance(nullableWithoutNull) // type check error as expected
isHighMaintenance(nullableWithNull)    // type check error as expected

handle<NullableString>(nullableWithoutNull)
handle<NullableString>(nullableWithNull)
Inigo
  • 12,186
  • 5
  • 41
  • 70
  • I've just tried that, but unfortunately this doesn't work. Not sure if that's just my TS version (v4.5.3). – Dawid Zbiński Dec 11 '21 at 23:27
  • Huh, just tried that in Stackblitz and in works, so it looks like an issue with my setup. Will try to figure that out. Thanks. Here's a StackBlitz, if anyone's interested: https://stackblitz.com/edit/angular-am5wj5?file=src/app/app.component.ts – Dawid Zbiński Dec 11 '21 at 23:47
  • 1
    I also just added proof code to my answer. It passes too. – Inigo Dec 11 '21 at 23:55
  • Managed to simulate my case in the StackBlitz above, the issue is that the T itself can be of type "something | undefined". – Dawid Zbiński Dec 12 '21 at 00:12
  • try my stricter update. See comments at the bottom of the test code. This should prevent non-typesafe calls. But if your underlying problem that you are converting "gappy" arrays to non gappy ones and TS doesn't recognize that, see https://stackoverflow.com/questions/43118692/typescript-filter-out-nulls-from-an-array and https://stackoverflow.com/questions/49089639/convincing-typescript-compiler-that-filtered-array-contains-no-nulls – Inigo Dec 12 '21 at 00:34