【问题标题】:Typing a redirect HOC in TypeScript with Next.js使用 Next.js 在 TypeScript 中键入重定向 HOC
【发布时间】:2021-03-23 15:44:41
【问题描述】:

我正在使用 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);

我觉得我很接近,但不能完全理解最后一个错误。 &lt;Component {...props} /&gt; 大喊:

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;

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

如何键入这两个函数?

编辑:

在 Linda 的帮助下,hoistStatics 现在可以工作了。 Redirect 仍然会导致问题。 I asked a new question for this here.

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 next.js typescript-typings react-props higher-order-components


    【解决方案1】:

    错误

    Typescript 在将来自各种扩展操作的道具拼凑在一起时可能会出错,因为边缘情况会导致无效的对象。

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

    组件 props T 可以是任何对象。如果T 包含必需的属性shouldRedirect,则打破这种情况的边缘情况。

    当我们写{shouldRedirect, ...props} 时,我们将shouldRedirect 解构到远离道具的地方。因此要知道这个props 变量不可能包含变量shouldRedirect。它是类型Omit&lt;T, 'shouldRedirect'&gt;。如果我们处于T 具有必需的shouldRedirect 属性的边缘情况下,那么我们在这里遇到问题:&lt;Component {...props} /&gt; 因为props 不能满足T 的要求。

    您得到的错误很难阅读,因为它输出的类型非常冗长,但该类型表示props 对象。 “'T' 可以用任意类型实例化”基本上意味着这些道具可能满足T 或者它们可能不满足T,这取决于T 是什么。

    解决方案

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

    function <T>(Component: React.ComponentType<Omit<T, 'shouldRedirect'>>)
    

    打字提升统计

    hoistStatics 是一个接受 HOC 并返回 HOC 的函数。我们知道 HOC 可以操作 props,使得组合组件 (Outer) 的 props 与基础组件 (Inner) 的 props 不同。所以我们可以在这里使用两个泛型。无论这两种类型是什么,hoistStatics 都不应更改它们,而应返回一个与参数具有相同签名的 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;
    };
    

    【讨论】:

    • 嗨,非常感谢! hoistStatics 完美运行。我用来自Redirect 的新错误修正了我的问题。如果您也能帮我解决这个问题,我们将不胜感激。
    • 我必须从 react-redux 类型中查看 InferableComponentEnhancerWithPropsMatching 等类型的详细信息。
    • 我会接受你的回答并给你赏金并提出一个新问题。如果你再看看,非常感谢!如果没有,你已经帮了我很多了! ?
    猜你喜欢
    • 2020-09-18
    • 1970-01-01
    • 2018-05-20
    • 2021-04-29
    • 2019-07-09
    • 1970-01-01
    • 1970-01-01
    • 2021-02-25
    相关资源
    最近更新 更多