Reputation: 368
I am trying to wrap an existing react component (react-select) in a High Order Component (HoC) in order to provide some conditional rendering logic. The difficulty I am facing is getting TypeScript to generate a component that unions the HoC wrapper and Wrapped components properties.
For example:
import React from "react";
import Select from "react-select";
interface HOCProps {
foo: string
}
// Return a type with the HOCProps properties removed.
type WitoutPrefilled<T extends HOCProps> = Pick<T, Exclude<keyof T, 'foo'>>;
function withHoC<P extends HOCProps>(WrappedComponent: React.ComponentType<P>) {
return class SomeHOC extends React.Component<WithoutPrefilled<P>> {
public render(): JSX.Element {
return <WrappedComponent {...this.props as P} /*foo={"test"}*/ />;
}
};
}
// Generate a wrapped component with a property union of the wrapped Select and outer HoC (HOCProps & Select) ?
const Wrapped = withHoC(Select);
What is the correct way to accomplish this?
React 16.8.3 TypeScript 3.3.3
Upvotes: 2
Views: 3586
Reputation: 368
Ok - so I've managed to get this working. The current solution satisfies two crucial requirements for me:
Infers defaultProps
for the child component that is being wrapped if they are present meaning the component generated by the HoC wrapper does not need to explicitly pass them.
Expose a union of the HoC wrappers properties and the underlying child components' properties so that Intellisense shows all available properties on the wrapped component.
In my quest to try and simplify what's actually going on the types expected by the withHoC
function are hardcoded (in my case react-select
), so it the withHoC
wrapper will only accept a react-select
Select
component to wrap. Anything else will probably throw type errors.
This link describes some code that may be able to infer the type of the component to be wrapped by withHoC
automatically, making withHoC
reusable with component types other than react-select's
Select
.
// node dependencies used (because dependencies mutate so much this may not work in other versions):
// "react": "^16.8.2",
// "react-dom": "^16.8.2",
// "typescript": "^3.3.3",
// "react-select": "2.4.1",
// "@types/react": "^16.8.3",
// "@types/react-dom": "^16.8.2",
// "@types/react-select": "^2.0.13",
// Visual Studio 2017 TypeScript SDK build 3.3.3
import ReactDOM from "react-dom"; // (Optional - just for testing)
import React from "react";
import Select from "react-select";
// Properties shape for React Select (See react-select @type definitions)
import { Props } from "react-select/lib/Select";
// The properties we are want to add so that our resultant wrapped component contains all of its own properties plus the extra properties specified here
interface IHOCProps {
bar: string;
}
function withHoC(WrappedComponent: React.ComponentType<Props>) {
return class SomeHOC extends React.Component<IHOCProps & Props> {
// If 'bar' isn't specified, configure a default (this section is optional)
static defaultProps = {
bar: "default bar"
};
public render(): JSX.Element {
return <><div>{this.props.bar}</div><WrappedComponent {...this.props as any} /></>;
}
};
}
const WrappedSelect = withHoC(Select);
export { WrappedSelect, Select };
// Test it out (Optional). If using Visual Studio 2017 or some other IDE with intellisense,
// <WrappedSelect /> should show all the 'react-select' properties and the HoC property (bar).
// Additionally, all the defaultProps for 'react-select' are automatically inferred so no TypeScript errors about missing props when using <WrappedSelect />.
const TestMe = () =>
<>
<WrappedSelect bar="bumble monkey">
<WrappedSelect />
<Select />
</>;
// Append the result to an HTML document body element
ReactDOM.render(<TestMe />,document.getElementsByTagName("body")[0]);
Somewhat pyrrhic victory since it's not reusable across types but it does work.
One final nugget; if you use Visual Studio 2017 and node for TypeScript, make sure the TypeScript SDK version is in sync with your npm node package, otherwise the IDE can report errors which don't exhibit themselves when doing a command line compilation (this caused me no end of red-herring problems).
Microsoft published this Url, which is infrequently updated and probably obsolete but no one in corporate has noticed.
The latest SDK can always be found on GitHub here which is typically always published alongside the npm and nuget packages.
Upvotes: 0
Reputation: 25790
First, make sure you Higher Order Component supplies the props your component is interested in. Take className
for example.
interface WithClassName {
className: string;
}
Our withClassName
HOC would take a component ready to accept className
as a prop, and return a component that does not accept className
anymore.
export function withClassName<T extends React.ComponentType<Partial<WithClassName>>>(Component: T): React.FunctionComponent<Omit<React.ComponentProps<T>, keyof WithClassName>> {
return props => React.createElement(Component, { className: "foo", ...props });
}
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
Usage:
const NewSelect = withClassName(Select);
Upvotes: 1