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