Typescript user-defined type guard checking object has all properties in array
Asked Answered
I

1

3

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;

playground link


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).


  1. Am I going about this wrong? Is there a better way to write this type guard?
  2. If this approach is fine, how can I tell typescript that I don't care if the type guard is "unsafe" per se?
Interlocutory answered 23/1, 2020 at 6:18 Comment(2)
"I'm trying to write a user-defined type guard that asserts that the value it's given has all the properties in a given array." What is the purpose of the RemoveNullables type towards this aim? It seems like you have some requirement about nullable properties which you haven't described in the question. – Nevanevada
The purpose of RemoveNullables is to create a type derived from any T where all the properties of T have their "nullable" (null, undefined, and ? optional) modifiers removed. So if I have a type type Foo = { foo?: string | null | undefined } the result of RemoveNullables<Foo> would simply be { foo: string }. The playground link might help communicate what I'm trying to achieve better. – Interlocutory
N
5

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:

  • The 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.
  • I took the liberty of simplifying the hasAll function implementation a bit.

Playground Link

Nevanevada answered 23/1, 2020 at 6:55 Comment(0)

© 2022 - 2024 β€” McMap. All rights reserved.