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?
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. Commented Jan 23, 2020 at 6:27
  • 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. Commented Jan 23, 2020 at 6:33

1 Answer 1

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

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.