Creating a redirect higher order component with TypeScript in Next.js

I am currently working on a Next.js project using TypeScript. My goal is to create a HOC that will redirect the user based on the Redux state.

Here is my current progress:

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);

I feel like I'm getting close, but I can't seem to understand the last error message. <Component {...props} /> gives me this error:

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">>'.

What does this mean? I managed to resolve the issue by typing the inner HOC like so:

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;
});

However, this means that Component ends up receiving a prop called shouldRedirect, which it shouldn't. It should only receive and pass along its usual props.

I had to disable two warnings:

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

And

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

hoistStatics is a higher-order HOC and here is how it looks like:

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;

The types for these functions are also makeshift (as seen in the disabled warning).

How can I properly type these two functions?

EDIT:

Thanks to Linda's guidance, the hoistStatics function now works correctly. However, I am still facing issues with the Redirect function. More information on this can be found 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);
  });
}

The line containing connector(Redirect) continues to generate errors:

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>>>'.

Answer №1

Identifying the Issue

While working with Typescript, errors can arise when combining props using spread operations due to specific edge cases that may result in an invalid object.

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

In this scenario, the T representing component props could be of any type. The issue arises if T contains a mandatory property like shouldRedirect.

The problem stems from destructuring shouldRedirect in {shouldRedirect, ...props}, leading to props lacking the shouldRedirect variable. It essentially becomes type Omit<T, 'shouldRedirect'>. Consequently, when T necessitates shouldRedirect, <Component {...props} /> would fail to meet the requirement.

The error message might be confusing due to its verbose nature, but it essentially represents the props object. "'T' could be instantiated with an arbitrary type" implies that these props may or may not align with T's requirements, depending on the actual type of T.

Suggesting a Fix

This issue commonly arises when dealing with Higher Order Components (HOCs), and resorting to {...props as T} can be a workaround. One approach is passing the entire object, similar to what's demonstrated in your "I can make the code pass" snippet. To ensure type safety by eliminating unwanted properties, we need to prevent the problematic edge case by specifying that our Component cannot demand this prop through the use of Omit.

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

Defining Typing for hoistStatics

hoistStatics functions by taking an HOC and returning another HOC. An HOC modifies props such that those of the composed component (Outer) differ from the base component (Inner). This necessitates two generics. Regardless of their types, hoistStatics must retain them while returning an HOC mirroring its argument.

We establish a general HOC signature for clarity:

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

Subsequently, we employ this type definition for 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;
};

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Methods for excluding individual packages from bundling

I am looking to exclude specific packages from being bundled together in my application. The documentation provides guidance on how to do so: /** @type {import('next').NextConfig} */ const nextConfig = { serverExternalPackages: ['package- ...

When I utilize nuxt alongside TypeScript in conjunction with koa and execute the "dev" command, the terminal alerts me of compilation issues within Vue files that incorporate TypeScript syntax

nuxt: 2.6.2 node: 16.5.0 koa: 2.7.0 Here is the content of my package.json file : { ... "scripts": { "dev": "cross-env NODE_ENV=development nodemon server/index.ts --watch server --exec babel-node --presets @babel/ ...

TS2339: The object of type 'Y' does not contain a property named 'x'

Confusion arises as to why the TypeScript error is triggered in this piece of code. (Please disregard the irrelevant example given): interface Images { [key:string]: string; } function getMainImageUrl(images: Images): string { return images.main; } E ...

Next.js is throwing a TypeError because it is unable to convert undefined or null to an object

Whenever I try to run my project locally, I encounter the 'TypeError: Cannot convert undefined or null to object' error as shown in the screenshot below. I've attempted updating all the packages listed in package.json. Furthermore, I delete ...

An error occurs when attempting to access a property that does not exist on type 'never'. Why is this considered an error rather than a warning?

I am experiencing an issue with the following code snippet: let count: number | undefined | null = 10; count = null; let result: string | undefined | null = count?.toFixed(2); console.log(`Result: ${result}`); The error message I received is as follows: ...

Error Message: "Required field 'client_id' missing in NextAuth Github Provider"

I've implemented next auth for handling authentication. The code snippet for my GithubProvider is as follows - import GitHubProvider from "next-auth/providers/github"; import GoogleProvider from "next-auth/providers/google"; expo ...

What is the best way to construct an object in Typescript that includes only properties that are not null or undefined?

Below is a sample list of properties: const firstProperty = this.checkSome(param) ? this.someArray[1] : null; const secondProperty = this.anotherProperty ? this.anotherProperty.id : null; I am looking to create an object using these properties: myObject = ...

Navigating a relative path import to the correct `@types` in Typescript

When the browser runs, I require import * as d3 from '../lib/d3.js'; for d3 to be fetched correctly. I have confirmed that this does work as intended. However, the file that contains the above code, let's call it main.js, is generated from ...

The phenomenon of NodeJS's string concatenation behaving unexpectedly

I need help creating a string to print a specific message. "At position #[" + index + "][" + _subIndex + "] TLPHN_DVC_TYP " + + _telNum?.TelephoneDeviceType.toString() + " ,allowed &quo ...

Creating personalized mapping for TypeScript objects

I have a TypeScript object structure that resembles the following: { "obj1" : { object: type1;}; "obj2" : { object: type2;}; "obj3" : { object: type3;}; "obj4" : { object: type4;}; "obj5" ...

Error: 'ngForOf' is not recognized as a valid property of the 'tr' element

Since this afternoon, I've been facing a challenge that I can't seem to grasp. The issue lies within a service I created; in this file, there is an object from which I aim to showcase the data in a loop. An error message is displayed: NG0303: C ...

Updating variable in a higher-level component in Angular 7

Currently, I am utilizing Angular 7. Within my child component displayed in the Stackblitz example below, I have encountered an obstacle. Although I can access my variable on the parent control by using @Input, I am unable to change it. Could you provide g ...

What is the process for obtaining a check_circle symbol with a black checkmark color instead of the default white in Angular Material?

Here is the code snippet where I am attempting to display a black tick inside a circle: <mat-icon [ngStyle]="{ color: selectedColor === color.checkedCircleColor ? color.checkedCircleColor : color.innerCircleColor}" >{{ sele ...

How can I define the True function using Typescript?

Currently, I am working on converting Javascript examples to typed Typescript as part of the "Flock of Functions" series. You can find the reference code at https://github.com/glebec/lambda-talk/blob/master/src/index.js#L152. The True function returns the ...

Unable to smoothly expand Div in React using Tailwind

I'm facing a challenge in animating a div that contains input elements. I want it to expand smoothly - initially, the text area is small and the other two inputs and buttons are hidden. When clicking on the text area, the input and button should be re ...

Having trouble resolving this issue: Receiving a Javascript error stating that a comma was expected

I am encountering an issue with my array.map() function and I'm struggling to identify the problem const Websiteviewer = ({ web, content, styles, design }) => { const test = ['1' , '2'] return ( {test.map(item => { ...

Guide to organizing documents using an interface structure

I currently have an interface that outlines the structure of my documents within a specific collection: interface IGameDoc { playerTurn: string; gameState: { rowOne: [string, string, string] rowTwo: [string, string, string] ...

Deduce the argument type of a class from the super call

I'm currently working on a project utilizing the discord.js library. Within this project, there is an interface called ClientEvents which contains different event argument tuple types: interface ClientEvents { ready: []; warn: [reason: string] m ...

Obtaining a customized variation of a class identified by a decorator

I am working with a class that is defined as follows: class Person { @OneToOne() pet: Animal; } Is there a method to obtain a transformed type that appears like this? (Including {propertyKey}Id: string to properties through the use of a decorator) ...

Guide for creating a function that accepts an array containing multiple arrays as input

I am working with a function called drawSnake and it is being invoked in the following manner: drawSnake( [ [0, 0], [0, 1], [0, 2], [0, 3], [0, 4], ] ); How should I format the input for this function? I have attempted using Array<Array<[numb ...