2

React's useCallback is a wrapper around a function that returns the same type as the input, here's the TS type:

function useCallback<T extends (...args: any[]) => any>(
  callback: T,
  deps: DependencyList,
): T;

If I don't specify the argument type for the input function, I'd expect noImplicitAny to cause an error. However, this doesn't cause an error:

const fn = useCallback(arg => {}, []);

It's possible to change useCallback's type to this to trigger an error in most cases when the argument's type isn't specified:

function useCallback<T extends (...args: never[]) => unknown>(
  callback: T,
  deps: DependencyList,
): T;


const fn = useCallback(arg => arg.toString()); // Property 'toString' does not exist on type 'never'.

const fn = useCallback((arg: number) => arg.toString()); // No errors

However, this seems to have issues in some cases. E.g. with default arguments:

const fn = useCallback((arg = 0) => arg.toString()); // Type 'number' is not assignable to type 'never'.

Usually, TS infers the type of arg. If I manually specify the type of arg, then Eslint's @typescript-eslint/no-inferrable-types rule produces an error.

Is there a better type for useCallback that addresses these issues?

Leo Jiang
  • 24,497
  • 49
  • 154
  • 284

1 Answers1

0

Let's take a look on useCallback types:

    /**
     * `useCallback` will return a memoized version of the callback that only changes if one of the `inputs`
     * has changed.
     *
     * @version 16.8.0
     * @see https://reactjs.org/docs/hooks-reference.html#usecallback
     */
    // TODO (TypeScript 3.0): <T extends (...args: never[]) => unknown>
    function useCallback<T extends (...args: any[]) => any>(callback: T, deps: DependencyList): T;

As you might have noticed, callback is a subtype of (...args: any[]) => any. It means that any is explicitly provided for args. This is why noImplicitAny does not work in this case, because any is explicit. As far as I understood and you have noticed - react team wants to replace current callback type with (...args: never[]) => unknown.

This subtype provides never type for callback argument.

useCallback((arg) => arg.toString(), []); // error because [arg] is never

arg = 0 does not work in a case of never because never is a bottom type. You are not allowed to assign any/any type to never. Consider this example:

let x: never;
let y: any;

x = y; // error

never is assignable to everything but not vice versa. Please see the docs

The never type is a subtype of, and assignable to, every type; however, no type is a subtype of, or assignable to, never (except never itself). Even any isn’t assignable to never.

By in most cases when the argument's type isn't specified I assume you want to forbid using any. I'm not sure whether react team can provide such restriction, because many people are using any during the migration from js to ts.

If you want to use default parameter, type of argument should be either any or has the same type as default parameter. Hence, we have a collision. You want to disallow any and at the same type you want to use default parameter.

You are able to override useCallback type in this way:

// credits goes to https://stackoverflow.com/questions/55541275/typescript-check-for-the-any-type
type IfAny<T, Y, N> = 0 extends 1 & T ? Y : N;
type IsAny<T> = IfAny<T, true, false>;

type ValidateArguments<T extends any[]> = IsAny<
  T[number]
> extends true
  ? 'Custom Error'[] // please provide any type you want
  : T;

type HandleCallback<T extends (...args: any[]) => any> = (
  ...args: ValidateArguments<Parameters<T>>
) => ReturnType<T>;

declare module "react" {
  function useCallback<T extends (...args: any[]) => any>(
    callback: HandleCallback<T>,
    deps: any[]
  ): T;
}

const App = () => {
  const result = useCallback((arg /** "Custom Error" */) => {}, []);
};

Playground

Instead CustomError you can use any type you want. However, using <T extends (...args: never[]) => unknown> might be a better solution than mine.