网站搜索

javascript - 为使用 useCallback 包装的函数启用 noImplicitAny?


React 的 useCallback 是一个函数的包装器,它返回与输入相同类型的值,下面是 TypeScript 类型:

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

如果我没有为输入函数指定参数类型,我期望 noImplicitAny 会引发错误。然而,这并不会引发错误:

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

可以将 useCallback 的类型更改为此类型,以在大多数情况下当参数类型未指定时触发错误:

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

然而,这在某些情况下似乎会有问题。例如,使用默认参数时:

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

通常,TS 会推断 arg 的类型。如果我手动指定 arg 的类型,那么 Eslint 的 @typescript-eslint/no-inferrable-types 规则会报错。

有没有一种更好的 useCallback 类型来解决这些问题?

答案#1

让我们来看一下 useCallback 的类型:

    /**
     * `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;

正如你可能已经注意到的,callback 是 (...args: any[]) => any 的子类型。这意味着 any 已经被明确提供给 args。这就是为什么在这种情况下 noImplicitAny 不起作用,因为 any 是明确的。根据我的理解,以及你注意到的——React 团队希望用 (...args: never[]) => unknown 来替换当前的 callback 类型。

此子类型为回调参数提供 never 类型。

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

arg=0 在 never 的情况下不起作用,因为 never 是底类型。你不被允许将任何/any 类型赋值给 never。请考虑这个例子:

let x: never;
let y: any;

x = y; // error

never 可以赋值给所有类型,但反过来不行。请查看文档

never 类型是每种类型的子类型,并且可以赋值给每种类型;然而,没有任何类型是 never 的子类型,也不能赋值给 never(除了 never 本身)。甚至 any 也不能赋值给 never。

我猜测你在说的 在大多数情况下,当参数的类型未指定时 是想禁止使用 any。我不确定 React 团队是否能提供这样的限制,因为很多人在从 js 迁移到 ts 时都在使用 any。

如果你想使用默认参数,参数的类型应该是 any 或者与默认参数类型相同。因此,我们有一个冲突。你想禁止使用 any,同时又想使用默认参数。

你可以通过这种方式覆盖 useCallback 的类型:

// 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" */) => {}, []);
};

操场

除了 CustomError,你可以使用任何你想要的类型。然而,使用 <T extends (...args: never[]) => unknown> 可能比我的方案更好。