I am using recompose in React, in particular its compose
function which has a signature of
function compose<TInner, TOutter>(
...functions: Function[]
): ComponentEnhancer<TInner, TOutter>;
interface ComponentEnhancer<TInner, TOutter> {
(component: Component<TInner>): ComponentClass<TOutter>;
}
where I am passing in some React higher order components. I defined my TInner
and TOuter
types as interfaces that describe what I need in order for my HoCs to function:
interface IOuterProps {
somethingYouGiveMe: string;
}
interface IInnerProps {
somethingIGiveBack: number;
}
const myEnhancer = compose<IInnerProps, IOuterProps>(myHoC, myOtherHoc);
(as a silly example for demonstration). But the problem is, my components have more props than that, the HoC that compose generates needs to take those extra props and pass them through, for example:
interface IMyComponentProps {
somethingYouGiveMe: string;
somethingElse: string;
}
Because of this, I can't do const MyEnhancedComponent = myEnhancer(MyComponent)
, as the compiler will complain that MyEnhancedComponent
does not have somethingElse
as a prop.
I found two work arounds, neither I am happy with. So curious what more experienced TypeScript devs would do.
By introducing a function, I can use generics and express to the compiler what I am doing
function myEnhancer<TInnerProps extends IInnerProps, TOuterProps extends IOuterProps>(Component: React.ComponentType<TInnerProps>) {
return compose<TInnerProps, TOuterProps>(mYHoC, myOtherHoc)(Component);
}
I really dislike this, introducing a function that will create multiple copies of the enhancer, just to get at generics? Seems wrong to me.
I can instead change my interfaces to be
interface IOutterProps {
somethingYouGiveMe: string;
[key: string]: any;
}
interface IInnerProps extends IOuterProps {
somethingIGiveBack: number;
}
I am basically using [key:string]: any
as a poor man's extends IOutterProps
. As in, I need you to give me these props, and if you give me extras then I don't really care about those.
This allows me to use myEnhancer
where ever I have components that meet the requirements, avoids the added function, and feels better than workaround #1. But also it feels wrong to have to add [key: string]: any
.
You can make the function signature inside ComponentEnhancer
generic:
declare function compose<TInner, TOutter>(
...functions: Function[]
): ComponentEnhancer<TInner, TOutter>;
interface ComponentEnhancer<TInner, TOutter> {
// This is now a generic function
<TActualInner extends TInner, TActualOuterProps extends TOutter>(component: React.ComponentClass<TActualInner>): React.ComponentClass<TActualOuterProps>;
}
interface IOuterProps {
somethingYouGiveMe: string;
}
interface IInnerProps {
somethingIGiveBack: number;
}
const myEnhancer = compose<IInnerProps, IOuterProps>();
interface IMyComponentProps {
somethingYouGiveMe: string;
somethingElse: string;
}
interface IMyInnerComponentProps {
somethingElse: string;
somethingIGiveBack: number;
}
class MyComponent extends React.Component<IMyInnerComponentProps>{ }
// We specify the actual inner and outer props
const MyEnhancedComponent = myEnhancer<IMyInnerComponentProps, IMyComponentProps>(MyComponent)
If we consider the logic of how the transformation works we could even use some conditional type magic to avoid specifying the type arguments explicitly. If my understanding of compose
is right, what happens is that the resulting component will have all the properties of the inner component excluding the properties of IInnerProps
and including the properties of IOuterProps
:
type Omit<T, TOmit> = { [P in Exclude<keyof T, keyof TOmit>] : T[P] }
type ComponentEnhancerProps<TActualInner, TInner, TOuter> = Omit<TActualInner, TInner> & TOuter;
interface ComponentEnhancer<TInner, TOutter> {
<TActualInner extends TInner>(component: React.ComponentClass<TActualInner>): React.ComponentClass<ComponentEnhancerProps<TActualInner, TInner, TOutter>>;
}
class MyComponent extends React.Component<IMyInnerComponentProps>{ }
const MyEnhancedComponent = myEnhancer(MyComponent)
let d = <MyEnhancedComponent somethingYouGiveMe="0" somethingElse="" />
The problem with this automated approach is that optional fields become required, you can keep optionality, if you have strictNullChecks
enabled
type ExcludeUndefined<T, TKeys extends keyof T> = { [P in TKeys]: undefined extends T[P] ? never : P}[TKeys];
type Omit<T, TOmit> =
{ [P in ExcludeUndefined<T, Exclude<keyof T, keyof TOmit>>] : T[P] } &
{ [P in Exclude<Exclude<keyof T, keyof TOmit>, ExcludeUndefined<T, Exclude<keyof T, keyof TOmit>>>] ? : T[P] }
type ComponentEnhancerProps<TActualInner, TInner, TOuter> = Omit<TActualInner, TInner> & TOuter;
interface ComponentEnhancer<TInner, TOutter> {
<TActualInner extends TInner>(component: React.ComponentClass<TActualInner>): React.ComponentClass<ComponentEnhancerProps<TActualInner, TInner, TOutter>>;
}
interface IMyInnerComponentProps {
somethingElse: string;
somethingIGiveBack: number;
optionalProp?: number;
}
class MyComponent extends React.Component<IMyInnerComponentProps>{ }
const MyEnhancedComponent = myEnhancer(MyComponent)
let d = <MyEnhancedComponent somethingYouGiveMe="0" somethingElse="" />