In React with material-ui I am trying to create a JSX component that accepts generic parameters and also uses the withStyles
HOC to inject my styles.
Th
The more I think about the question, the more I like Frank Li's approach. I'd make two modifications: (1) introduce an extra SFC to avoid a cast, and (2) grab the outer props type from the wrapped component C
instead of hard-coding it. (If we hard-coded Props<T>
, TypeScript would at least check that it is compatible with this.C
, but we are at risk of requiring props that this.C
doesn't actually require or failing to accept optional props that this.C
actually accepts.) It's jaw-dropping that referencing a property type from a type argument in the extends
clause works, but it seems to!
class WrappedBaseFormCard<T> extends React.Component<
// Or `PropsOf<WrappedBaseFormCard<T>["C"]>` from @material-ui/core if you don't mind the dependency.
WrappedBaseFormCard<T>["C"] extends React.ComponentType<infer P> ? P : never,
{}> {
private readonly C = withStyles(styles)(
// JSX.LibraryManagedAttributes handles defaultProps, etc. If you don't
// need that, you can use `BaseFormCard<T>["props"]` or hard-code the props type.
(props: JSX.LibraryManagedAttributes<typeof BaseFormCard, BaseFormCard<T>["props"]>) =>
<BaseFormCard<T> {...props} />);
render() {
return <this.C {...this.props} />;
}
}
I think any complaints about runtime overhead of this approach are probably nonsense in the context of a whole React application; I'll believe them when someone presents data supporting them.
Note that Lukas Zech's approach using an SFC is very different: every time the props to the outer SFC change and it is called again, withStyles
is called again, generating a wrapper
that looks to React like a whole new component type, so React throws away the old wrapper
instance and a new inner BaseFormCard
component gets created. This would have undesirable behavior (resetting state), not to mention greater runtime overhead. (I haven't actually tested this, so let me know if I'm missing something.)
This works enough for me using Visual Studio:
import React from "react";
import { withStyles, WithStyles } from "@material-ui/styles";
const styles = {
root: { ... },
};
export interface Props<T> { ... }
export interface State<T> { ... }
export class TableComponent<T> extends React.PureComponent<Props<T> & WithStyles<typeof styles>, State<T>> {
render () {
return <div className={this.props.classes.root} />;
}
}
export const Table = (withStyles(styles)(TableComponent) as any) as new <T>() => TableComponent<T>;
export default Table;
Here is a solution that doesn't create a wrapper component. Instead it creates a type that is like the component type just without the 'classes' property.
// TypeUtils.tsx
// from: https://stackoverflow.com/questions/48215950/exclude-property-from-type
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
Using this helper type you can create the type you want and cast your styled component to that type
type FixedCheckBoxType = <T>(props: Omit<FormCheckBoxProps<T>, 'classes'>) => JSX.Element;
export const FormCheckBox = withStyles(checkboxStyles)(UnstyledFormCheckBox) as FixedCheckBoxType;
There might be a better way to do this, but ideally this would be done automatically by material-ui itself.
There needs to be an overload of withStyles
typings for every possible number of type parameters