EDIT: Here is a link to a minimal reproducible example. I had to include the external libraries Redux and React Router Dom to reproduce it faithfully.
I'm building a Next.js app with Redux and TypeScript.
I have a higher-order higher-order component that hoists statics.
import hoistNonReactStatics from 'hoist-non-react-statics';
type HOC<Inner, Outer = Inner> = (
Component: React.ComponentType<Inner>,
) => React.ComponentType<Outer>;
const hoistStatics = <I, O>(higherOrderComponent: HOC<I, O>): HOC<I, O> => (
BaseComponent: React.ComponentType<I>,
) => {
const NewComponent = higherOrderComponent(BaseComponent);
hoistNonReactStatics(NewComponent, BaseComponent);
return NewComponent;
};
export default hoistStatics;
Using this, I want to write a redirect
HOC that can redirect users based on Redux state. Here is what I have.
import Router from 'next/router.js';
import { curry } from 'ramda';
import { useEffect } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { RootState } from 'redux/root-reducer';
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<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);
});
}
export default curry(redirect);
The problem here is that return connector(Redirect);
throws the following TS error.
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>>>'.ts(2345)
How can I fix this typing? I thought that telling TypeScript about the "any props" T
in hoist-statics
and then in Redirect
's props would suffice.
So there's an actual mistake and then there's a bunch of bullshit.
The actual mistake is that Redirect
needs to use PropsWithChildren
.
The bullshit has to do with the Matching
utility type from the react-redux package which is used for joining the injected props with the components own props.
/**
* A property P will be present if:
* - it is present in DecorationTargetProps
*
* Its value will be dependent on the following conditions
* - if property P is present in InjectedProps and its definition extends the definition
* in DecorationTargetProps, then its definition will be that of DecorationTargetProps[P]
* - if property P is not present in InjectedProps then its definition will be that of
* DecorationTargetProps[P]
* - if property P is present in InjectedProps but does not extend the
* DecorationTargetProps[P] definition, its definition will be that of InjectedProps[P]
*/
export type Matching<InjectedProps, DecorationTargetProps> = {
[P in keyof DecorationTargetProps]: P extends keyof InjectedProps
? InjectedProps[P] extends DecorationTargetProps[P]
? DecorationTargetProps[P]
: InjectedProps[P]
: DecorationTargetProps[P];
};
We get an error between these two types here, saying that A<T>
is not assignable to B<T>
.
type A<T> = PropsWithChildren<Matching<{ shouldRedirect: boolean; } & DispatchProp<AnyAction>, T & { shouldRedirect: boolean; } & DispatchProp<AnyAction>>>
type B<T> = PropsWithChildren<T & { shouldRedirect: boolean; } & DispatchProp<AnyAction>>
Those two types are basically the same 99.9% of the time. Again we are looking at what weird edge case would make it not true.
Basically the only time it doesn't work is if T
has a prop shouldRedirect
or dispatch
with a type that is narrower than what we injected, ie. true
instead of boolean
.
InjectedProps
is { shouldRedirect: boolean; } & DispatchProp<AnyAction>
DecorationTargetProps
is T & { shouldRedirect: boolean; } & DispatchProp<AnyAction>>
Honestly my brain is exploding thinking about which of the bullet point rules is the problem, but I can see the issue comparing lines 87 and 88 of this playground.
So at this point... just as any
and move on with your life.