0

I have a function which is called on a click event:

  useEffect(() => {
    document.addEventListener('click', (e) => handleClickOutside(e), true);
  });

The function:

  const myElement = useRef(null);

  const handleClickOutside = (e: MouseEvent) => {
    if (!colorPickerRef) {
      return;
    }

    if (!myElement.current?.contains(e.target)) {
         // do something
    }
  };

However I get the error property 'contains' does not exist on type 'never'

How can I promise typescript that myElement.current is not null?

sir-haver
  • 3,096
  • 7
  • 41
  • 85
  • Please note your `useEffect` will run, and thus attach an event listener, on every render. You should add an empty dependency array so that it only runs on the initial render, as well as return a cleanup function to remove the listener. – Riwen Oct 19 '22 at 11:01

2 Answers2

1

I think that the better way to handle this is to also pass to the useRef a type.

By this I mean something like this : useRef<HTMLDivElement> for example

const myElement = useRef<HTMLDivElement>(null)

const handleClickOutside = (e: MouseEvent) => {
    if (!myElement.current) {
        // If myElement is null then we return
        return
    }
    
    if (!myElement.current.contains(e.target)) {
            // do something
    }
}
adevinwild
  • 278
  • 4
  • 8
1

Problem

The type for useRef looks like this:

    /**
     * `useRef` returns a mutable ref object whose `.current` property is initialized to the passed argument
     * (`initialValue`). The returned object will persist for the full lifetime of the component.
     *
     * Note that `useRef()` is useful for more than the `ref` attribute. It’s handy for keeping any mutable
     * value around similar to how you’d use instance fields in classes.
     *
     * @version 16.8.0
     * @see https://reactjs.org/docs/hooks-reference.html#useref
     */
    function useRef<T>(initialValue: T): MutableRefObject<T>;
    // convenience overload for refs given as a ref prop as they typically start with a null value
    /**
     * `useRef` returns a mutable ref object whose `.current` property is initialized to the passed argument
     * (`initialValue`). The returned object will persist for the full lifetime of the component.
     *
     * Note that `useRef()` is useful for more than the `ref` attribute. It’s handy for keeping any mutable
     * value around similar to how you’d use instance fields in classes.
     *
     * Usage note: if you need the result of useRef to be directly mutable, include `| null` in the type
     * of the generic argument.
     *
     * @version 16.8.0
     * @see https://reactjs.org/docs/hooks-reference.html#useref
     */
    function useRef<T>(initialValue: T|null): RefObject<T>;
    // convenience overload for potentially undefined initialValue / call with 0 arguments
    // has a default to stop it from defaulting to {} instead
    /**
     * `useRef` returns a mutable ref object whose `.current` property is initialized to the passed argument
     * (`initialValue`). The returned object will persist for the full lifetime of the component.
     *
     * Note that `useRef()` is useful for more than the `ref` attribute. It’s handy for keeping any mutable
     * value around similar to how you’d use instance fields in classes.
     *
     * @version 16.8.0
     * @see https://reactjs.org/docs/hooks-reference.html#useref
     */
    function useRef<T = undefined>(): MutableRefObject<T | undefined>;

reference

It either returns a RefObject or a MutableRefObject. They essentially have the same shape and look like this:

    interface RefObject<T> {
        readonly current: T | null;
    }

reference

    interface MutableRefObject<T> {
        current: T;
    }

reference

From the above you can see the type of current takes on the type of the argument useRef is called with. You called useRef with null, therefore T has a type of null, therefore current has a type of null. The contains method cannot be called on null hence the TypeScript error.

Solution

useRef accepts a type parameter (see generics) which allows you to give a type to T yourself. You might do something like this (replace HTMLElement with a type more specific to your case):

import { useRef } from "react";

const myElement = useRef<HTMLElement>(null);

const handleClickOutside = (e: MouseEvent) => {
  if (!myElement.current?.contains(e.target)) {
    // do something
  }
};

TypeScript Playground

Wing
  • 8,438
  • 4
  • 37
  • 46
  • Thanks a lot both the solutions work but I still need to cast e.target like so: e.target as HTMLElement. Otherwise it won't work, I will read into all your references – sir-haver Oct 19 '22 at 11:05
  • Yea, that's a separate issue. Casting is fine. See related questions: https://stackoverflow.com/questions/61164018/typescript-ev-target-and-node-contains-eventtarget-is-not-assignable-to-node; https://stackoverflow.com/questions/28900077/why-is-event-target-not-element-in-typescript. – Wing Oct 19 '22 at 11:15
  • Thanks a lot, I did have to use HTMLDivElement though – sir-haver Oct 19 '22 at 15:39