使用 Next.js 在 TypeScript 中输入重定向 HOC

Typing a redirect HOC in TypeScript with Next.js

我正在使用 TypeScript 构建一个 Next.js 项目。我正在尝试输入一个基于 Redux 状态重定向用户的 HOC。

这是我目前的情况:

import { RootState } from 'client/redux/root-reducer';
import Router from 'next/router.js';
import { curry } from 'ramda';
import React, { useEffect } from 'react';
import { connect, ConnectedProps } from 'react-redux';

import hoistStatics from './hoist-statics';

function redirect(predicate: (state: RootState) => boolean, path: string) {
  const isExternal = path.startsWith('http');

  const mapStateToProps = (state: RootState) => ({
    shouldRedirect: predicate(state),
  });

  const connector = connect(mapStateToProps);

  return hoistStatics(function <T>(Component: React.ComponentType<T>) {
    function Redirect({
      shouldRedirect,
      ...props
    }: T & ConnectedProps<typeof connector>): JSX.Element {
      useEffect(() => {
        if (shouldRedirect) {
          if (isExternal && window) {
            window.location.assign(path);
          } else {
            Router.push(path);
          }
        }
      }, [shouldRedirect]);

      return <Component {...props} />;
    }

    return Redirect;
  });
}

export default curry(redirect);

我觉得我已经很接近了,但不能完全理解最后一个错误。 <Component {...props} /> 大喊:

Type 'Pick<T & { shouldRedirect: boolean; } & DispatchProp<AnyAction>, "dispatch" | Exclude<keyof T, "shouldRedirect">>' is not assignable to type 'IntrinsicAttributes & T & { children?: ReactNode; }'.
  Type 'Pick<T & { shouldRedirect: boolean; } & DispatchProp<AnyAction>, "dispatch" | Exclude<keyof T, "shouldRedirect">>' is not assignable to type 'T'.
    'T' could be instantiated with an arbitrary type which could be unrelated to 'Pick<T & { shouldRedirect: boolean; } & DispatchProp<AnyAction>, "dispatch" | Exclude<keyof T, "shouldRedirect">>'.

这是怎么回事?我可以通过像这样输入内部 HOC 来使代码通过:

return hoistStatics(function <T>(Component: React.ComponentType<T>) {
  function Redirect(
    props: T & ConnectedProps<typeof connector>,
  ): JSX.Element {
    useEffect(() => {
      if (props.shouldRedirect) {
        if (isExternal && window) {
          window.location.assign(path);
        } else {
          Router.push(path);
        }
      }
    }, [props.shouldRedirect]);

    return <Component {...props} />;
  }

  return Redirect;
});

但这意味着 Component 得到了一个叫做 shouldRedirect 的道具,它不应该。它应该只接收并传递它通常接收的道具。

我不得不禁用两个警告。

  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  // @ts-ignore
  return hoistStatics(function <T>(Component: React.ComponentType<T>) {

    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore
    return connector(Redirect);

hoistStatics 是高阶 HOC,看起来像这样。

import hoistNonReactStatics from 'hoist-non-react-statics';

const hoistStatics = (
  higherOrderComponent: <T>(
    Component: React.ComponentType<T>,
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
  ) => (props: T & any) => JSX.Element,
) => (BaseComponent: React.ComponentType) => {
  const NewComponent = higherOrderComponent(BaseComponent);
  hoistNonReactStatics(NewComponent, BaseComponent);
  return NewComponent;
};

export default hoistStatics;

它的类型也被 hack 在一起(正如您在禁用警告中看到的那样)。

这两个函数怎么打出来的?

编辑:

在 Linda 的帮助下 hoistStatics 现在可以工作了。 Redirect 仍然会导致问题。 .

function redirect(predicate: (state: RootState) => boolean, path: string) {
  const isExternal = path.startsWith('http');

  const mapStateToProps = (state: RootState) => ({
    shouldRedirect: predicate(state),
  });

  const connector = connect(mapStateToProps);

  return hoistStatics(function <T>(
    Component: React.ComponentType<Omit<T, 'shouldRedirect'>>,
  ) {
    function Redirect({
      shouldRedirect,
      ...props
    }: T & ConnectedProps<typeof connector>): JSX.Element {
      useEffect(() => {
        if (shouldRedirect) {
          if (isExternal && window) {
            window.location.assign(path);
          } else {
            Router.push(path);
          }
        }
      }, [shouldRedirect]);

      return <Component {...props} />;
    }

    return connector(Redirect);
  });
}

带有 connector(Redirect) 的行仍然抛出错误:

Argument of type '({ shouldRedirect, ...props }: T & { shouldRedirect: boolean; } & DispatchProp<AnyAction>) => Element' is not assignable to parameter of type 'ComponentType<Matching<{ shouldRedirect: boolean; } & DispatchProp<AnyAction>, T & { shouldRedirect: boolean; } & DispatchProp<AnyAction>>>'.
  Type '({ shouldRedirect, ...props }: T & { shouldRedirect: boolean; } & DispatchProp<AnyAction>) => Element' is not assignable to type 'FunctionComponent<Matching<{ shouldRedirect: boolean; } & DispatchProp<AnyAction>, T & { shouldRedirect: boolean; } & DispatchProp<AnyAction>>>'.
    Types of parameters '__0' and 'props' are incompatible.
      Type 'PropsWithChildren<Matching<{ shouldRedirect: boolean; } & DispatchProp<AnyAction>, T & { shouldRedirect: boolean; } & DispatchProp<AnyAction>>>' is not assignable to type 'T & { shouldRedirect: boolean; } & DispatchProp<AnyAction>'.
        Type 'PropsWithChildren<Matching<{ shouldRedirect: boolean; } & DispatchProp<AnyAction>, T & { shouldRedirect: boolean; } & DispatchProp<AnyAction>>>' is not assignable to type 'T'.
          'T' could be instantiated with an arbitrary type which could be unrelated to 'PropsWithChildren<Matching<{ shouldRedirect: boolean; } & DispatchProp<AnyAction>, T & { shouldRedirect: boolean; } & DispatchProp<AnyAction>>>'.

错误

Typescript 在拼凑来自各种传播操作的 props 时可能会出错,因为边缘情况会导致无效对象。

return hoistStatics(function <T>(Component: React.ComponentType<T>) {
    function Redirect({
      shouldRedirect,
      ...props
    }: T & ConnectedProps<typeof connector>): JSX.Element {
         /*...*/
      return <Component {...props} />;
    }

组件道具T可以是任何对象。如果 T 包含必需的 属性 shouldRedirect.

,则打破这一点的边缘情况

我们在编写 {shouldRedirect, ...props} 时从 props 中解构了 shouldRedirect。因此知道这个props变量不可能包含一个变量shouldRedirect。它是 Omit<T, 'shouldRedirect'> 类型。如果我们处于 T 具有必需的 shouldRedirect 道具的边缘情况,那么我们会在这里遇到问题:<Component {...props} /> 因为 props 无法满足 T.

你得到的错误很难阅读,因为它输出的类型太啰嗦了,但那个类型代表了 props 对象。 “'T' 可以用任意类型实例化”基本上意味着这些道具可能满足 T 或者它们可能不满足 T,具体取决于 T 是什么。

解决方案

这在编写 HOC 时经常出现,如果遇到这种情况,您总是可以求助于 {...props as T}。通常我所做的是传递整个对象,就像您在“我可以让代码通过”代码块中所做的那样。如果我们想要删除不需要的属性并保持其类型安全,我们需要禁止不良的边缘情况。我们保持 T 含糊不清,但声明我们的 Component 不能通过使用 Omit.

来要求这个道具
function <T>(Component: React.ComponentType<Omit<T, 'shouldRedirect'>>)

输入 hoistStatics

hoistStatics 是一个接受 HOC 的函数,returns 是一个 HOC。我们知道 HOC 可以操纵 props,使得组合组件 (Outer) 的 props 不同于基础组件 (Inner) 的 props。所以我们可以在这里使用两个泛型。无论这两种类型是什么,hoistStatics 都不应该更改它们并且应该 return 一个与其参数具有相同签名的 HOC。

我们定义了一个通用的 HOC 签名来保持干净

type HOC<Inner, Outer = Inner> = (
  Component: React.ComponentType<Inner>,
) => React.ComponentType<Outer>

然后我们用那个类型来描述hoistStatics

const hoistStatics = <I, O>(
  higherOrderComponent: HOC<I, O>
): HOC<I, O> => (BaseComponent: React.ComponentType<I>) => {
  const NewComponent = higherOrderComponent(BaseComponent);
  hoistNonReactStatics(NewComponent, BaseComponent);
  return NewComponent;
};