Reputation: 19202
I'm trying to write a user-defined type guard that tests whether the value it's given has all the properties in a given array.
I'm calling this function hasAll
and it's implementation and usage in Javascript would look like this:
function hasAll(obj, keysToCheck) {
if (!obj) return false;
for (const key of keysToCheck) {
const value = obj[key];
if (value === null) return false;
if (value === undefined) return false;
}
return true;
}
hasAll({ foo: 'test', bar: 5 }, ['foo', 'bar']); // true
hasAll({ foo: 'test', bar: 5 }, ['foo', 'bar', 'baz']); // false
What I'm trying to do now is turn the above function into a type guard. This is what I have so far:
// this _almost_ works 🔴
type Nullable<T> = T | null | undefined;
type RemoveNullables<T, K extends keyof T> = {
[P in K]-?: T[P] extends Nullable<infer U> ? U : T[P];
};
function hasAll<T, K extends keyof NonNullable<T>>(
obj: T,
keysToCheck: K[],
): obj is RemoveNullables<NonNullable<T>, K> {
// but i'm getting an error here 👆👆👆
if (!obj) return false;
const nonNullableObj = obj as NonNullable<T>;
for (const key of keysToCheck) {
const value = nonNullableObj[key];
if (value === null) return false;
if (value === undefined) return false;
}
return true;
}
export default hasAll;
The error message is:
A type predicate's type must be assignable to its parameter's type.
Type 'RemoveNullables<NonNullable<T>, K>' is not assignable to type 'T'.
I've read this answer with a good explanation however it doesn't really help my case.
I want to explicitly assert that my type T
will conform to RemoveNullables<NonNullable<T>, K>
after it runs through this function. I don't really care whether or not T
is assignable to RemoveNullables<NonNullable<T>, K>
(if that makes sense).
Upvotes: 3
Views: 2190
Reputation: 51034
This seems to meet your requirements:
type ExcludeNullable<T, K extends keyof NonNullable<T>> = NonNullable<T> & {
[k in K]-?: Exclude<NonNullable<T>[k], null | undefined>
}
function hasAll<T, K extends keyof NonNullable<T>>(
obj: T,
keysToCheck: K[]
): obj is ExcludeNullable<T, K> {
return obj !== null && obj !== undefined
&& keysToCheck.every(k => obj![k] !== null && obj![k] !== undefined);
}
A few notes:
T & ...
intersection type guarantees that ExcludeNullable<T, K>
is assignable to T
. Without this, the mapped type doesn't have properties of T
that are missing from K
.Exclude
is a simpler way to get rid of null
and undefined
than using a conditional type with infer
.hasAll
function implementation a bit.Upvotes: 5