Typescript with React - use HOC on a generic component class - reactjs

I have a generic React component, say like this one:
class Foo<T> extends React.Component<FooProps<T>, FooState> {
constructor(props: FooProps<T>) {
super(props);
render() {
return <p> The result is {SomeGenericFunction<T>()}</p>;
}
}
I also have a HOC that looks similar to this one (but is less pointless):
export const withTd =
<T extends WithTdProps>(TableElement: React.ComponentType<T>): React.SFC<T> =>
(props: T) => <td><TableElement {...props}/></td>;
But when I use a component like this:
const FooWithTd = withTd(Foo);
There is no way to pass the type argument, as you can do neither withTd(Foo<T>), nor can you do FooWithTd, the type is always wrong.
What is the proper way to do that?
EDIT: The problem is that I want to be able to have something like <FooWithTd<number> {...someprops}/> later on, as I don't know the desired type for T in the HOC.

You can wrap your component which is created from a HOC into another component. It would look something like this:
class FooWithTd<T> extends React.Component<SomeType<T>> {
private Container: React.Component<SomeType<T> & HOCResultType>;
constructor(props:SomeType<T>){
super(props);
this.Container = withTd(Foo<T>);
}
render() {
return <this.Container {...this.props} />;
}
}
Remember, you probably don't want the HOC inside your render function because it means that the component will be recreated every each render.

Thanks for asking this question. I just figured out a way to specify a type parameter to a component after wrapping it with an HOC and I thought I'd share.
import React from 'react';
import withStyles from '#material-ui/core/styles/withStyles';
import { RemoveProps } from '../helpers/typings';
const styles = {
// blah
};
interface Props<T> {
classes: any;
items: T[];
getDisplayName: (t: T) => string;
getKey: (t: T) => string;
renderItem: (t: T) => React.ReactNode;
}
class GenericComponent<T> extends React.Component<Props<T>, State> {
render() {
const { classes, items, getKey, getDisplayName, renderItem } = this.props;
return (
<div className={classes.root}>
{items.map(item => (
<div className={classes.item} key={getKey(item)}>
<div>{getDisplayName(item)}</div>
<div>{renderItem(item)}</div>
</div>
))}
</div>
);
}
}
// ๐Ÿ‘‡ create a `type` helper to that output the external props _after_ wrapping it
type ExternalProps<T> = RemoveProps<Props<T>, 'classes'>;
export default withStyles(
styles
)(GenericComponent) as <T extends any>(props: ExternalProps<T>) => any;
// ๐Ÿ‘† cast the wrapped component as a function that takes
// in a type parameter so we can use that type
// parameter in `ExternalProps<T>`
The main idea is to cast the wrapped component as a function that takes in a type parameter (e.g. T) and use that type parameter to derive the external props after the component has been wrapped.
If you do this, then you can specify a type parameter when using the wrapped version of GenericComponent e.g.:
<GenericComponent<string> {/*...*/} />
Hopefully the code is explanatory enough for those who still have this problem. In general though, I consider this relatively advanced typescript usage and it's probably easier to use any instead of a generic parameter in the props

Workaround: simple case
If your component's type parameter is used only for passing it to props, and users of the component do not expect it having any functionality beyond just passing props and rendering, you can explicitly hard-cast the result of your hoc(...args)(Component) to React's functional component type, like this:
import React, {ReactElement} from 'react';
class MyComponent<T> extends React.Component<MyProps<T>> { /*...*/ }
const kindaFixed = myHoc(...args)(MyComponent) as unknown as <T>(props: MyProps<T>) => ReactElement;
Workaround: more complex and with some runtime costs
You can use fabric-like function, supposed here:
class MyComponent<T> extends React.Component<MyProps<T>> { /*...*/ }
export default function MyComponentFabric<T>() {
return hoc(...args)(MyComponent as new(props: MyProps<T>) => MyComponent<T>);
}
This one will require you to create new version of wrapped component for each type you use it with:
import MyComponentFabric from '...whenever';
const MyComponentSpecificToStrings = MyComponentFabric<string>();
It will allow you to access all public instance fields and methods of your component.
Summary
I faced this issue when tried to use connect from react-redux on my ExampleGenericComponent<T>. Unfortunatelly, it cannot be fixed properly until TypeScript will support HKT, and any HOC you use will update its typings respecting this feature.
There is possibly no correct solution (at least for now) for usages beyond just rendering, when you need to access component instance fields and methods. By 'correct' I mean 'without ugly explicit typecasts', and 'with no runtime cost'.
One thing you can try is to split your class-component into two components, one that will be used with HOC, and other that will provide fields and methods that you need.

Just stumbled upon this as well and thought I'd share what I came up with in the end.
Building on what #rico-kahler provided, my approach mapped to your code would be
export const FooWithTd = withTd(Foo) as <T>(props: FooProps<T>) => React.ReactElement<FooProps<T>>;
which you can then use like this
export class Bar extends React.Component<{}> {
public render() {
return (
<FooWithTd<number> />
);
}
}
In my case, I have defaultProps as well and I inject props by ways of another HOC, the more complete solution would look like this:
type DefaultProps = "a" | "b";
type InjectedProps = "classes" | "theme";
type WithTdProps<T> = Omit<FooProps<T>, DefaultProps | InjectedProps> & Partial<FooProps<T> & { children: React.ReactNode }>;
export const FooWithTd = withTd(Foo) as <T>(props: WithTdProps<T>) => React.ReactElement<WithTdProps<T>>;

EDIT:
After some changes to your code, it was only a wrong constraint T in your withTd function.
// I needed to change the constraint on T, but you may adapt with your own needs
export const withTd = <T extends FooProps<WithTdProps>>(
TableElement: React.ComponentType<T>
): React.SFC<T> => (props: T) => (
<td>
<TableElement {...props} />
</td>
)
// Explicitly typed constructor
// Removed after EDIT
//const FooW = Foo as new (props: FooProps<WithTdProps>) => Foo<WithTdProps>
// Inferred as React.StatelessComponent<FooProps<WithTdProps>>
const FooWithTd = withTd(Foo)
No longer relevant after EDIT :
You may find more information at this issue https://github.com/Microsoft/TypeScript/issues/3960

Related

Hide generic type prop on export

I have a small set of components in which a Wrapper is going to manipulate its children (therefore referred to as Components by injecting a prop into each of the children via cloneElement.
The gotcha here is that Component props are of a generic type. When I expose Component on the code, I don't want one of its props to be on the signature, because it will be automatically injected by the Wrapper component.
I have a concise example which shows what I mean:
types.ts
export type SomeObject = {
someKey: string;
};
type PropThatWillBeInjected<T extends SomeObject> = {
fn: (value: string) => T;
};
export type WannaBePropTypes = {
name: string;
};
export type PropTypes<T extends SomeObject> = PropThatWillBeInjected<T> &
WannaBePropTypes;
Important: PropTypes<T> is what Component expects, but as a programmer, I want WannaBePropTypes to be the signature of this component.
Moving on...
Component.tsx
function Component<T extends SomeObject>(props: PropTypes<T>) {
const { fn, name } = props;
const result = fn(name);
return <div>Hello, {result.someKey}</div>;
}
export default Component;
Wrapper.tsx
function Wrapper(props: { children: ReactNode }) {
const { children } = props;
return (
<div id="wrapper">
{React.Children.map(
children as ReactElement<PropTypes<SomeObject>>,
(child, index) =>
cloneElement(child, {
...child.props,
fn: (value: string) => ({
someKey: `${value}-${index}`,
}),
})
)}
</div>
);
}
export default Wrapper;
As expected, when I try to use these components as the following, the code works but the compiler complains:
<Wrapper>
<Component name="Alice" />
<Component name="Bob" />
</Wrapper>
Property 'fn' is missing in type '{ name: string; }' but required in type 'PropThatWillBeInjected'.(2741)
Is there a way to cast Component so I don't need to pass fn manually? I know there's a way when the prop types is not generic...
What I've tried:
Making fn optional: works, but this is not the solution I'm looking for;
Wrapping Component with another component and passing a noop to Component: works, but I don't want to create this unnecessary wrapper;
A playground with this sample code: StackBlitz
If I inderstand your problem correctly, you want to call Component as <Component name="Alice" /> and there should be some internal logic for two cases: when fn was passed and when not. If so, you can create unnecessary type (instead of unnecessary wrapper) which will be one of WannaBePropTypes or full props. This is like some combination of your try#1 and try#2:
type FullProps<T extends SomeObject> = PropThatWillBeInjected<T> & WannaBePropTypes;
type PropTypes<T extends SomeObject> = FullProps<T> | WannaBePropTypes;
So fn is optional until you define children as ReactElement<FullProps<SomeObject>> in Wrapper component. This is how to tackle with Typescript only.
BTW: maybe you can just pass array of WannaBePropTypes objects into Wrapper instead of children? This sounds better if <Component name="Alice" /> should do nothing by itself.

Strictly typing React higher order component which consumes properties with TypeScript

I'm trying to type some higher order React components which are more complex than only injecting properties. These HOCs must be able to:
receive properties that are not (or only optionally) forwarded to the wrapped component
provide some new properties to the wrapped component
take additional arguments when they are created
From what I can tell, this means we must have three types:
The properties the HOC provides to the wrapped component
The properties the HOC uses, but which the wrapped component not necessarily cares about
The properties of the wrapped component that the HOC doesn't care about
I have the following code that works, but I can't find a way to avoid the any cast when passing the props on to the wrapped component:
import React, { ComponentType } from 'react';
// Props of the wrapped component without the props provided by the HOC
type StrictWrappedProps<WrappedProps, InjectedProps> = Omit<
WrappedProps,
keyof InjectedProps
>;
type WrappedComponent<WrappedProps, InjectedProps> = ComponentType<
WrappedProps & InjectedProps
>;
type HOCType<WrappedProps, InjectedProps, OwnProps> = ComponentType<
OwnProps & StrictWrappedProps<WrappedProps, InjectedProps>
>;
type LengthifyOwnProps = {
name: string;
};
type LengthifyInjectedProps = {
length: number;
};
export const lengthify = <WrappedProps extends {}>(
Wrapped: WrappedComponent<WrappedProps, LengthifyInjectedProps>,
multiplier: number
): HOCType<WrappedProps, LengthifyInjectedProps, LengthifyOwnProps> => ({
name,
...props
}) => {
const length = name.length * multiplier;
return <Wrapped {...props as any} length={length} />;
};
Here is an example usage of that HOC:
const myComp = ({ bool, length }: { bool: boolean; length: number }) => (
<p>
Bool is: {bool}. Length is: {length}
</p>
);
const EnhancedMyComp = lengthify(myComp, 2);
const usage = () => <EnhancedMyComp bool={true} name="Hello" />;
The any cast is a small workaround, but I would like to find the strict way of typing this.
The React TypeScript cheat sheet on HOCs links to this bug in the TypeScript repo, but it seems to be more or less solved, and only relates to HOCs that inject properties.
Other questions here on StackOverflow also seem to to be about injecting properties, or confusion about the ComponentType type.

React Component children typecheck with typescript

Here is the scenario:
I have a custom component:
class MyComponent extends React.Component {
render () {
return (
<SuperComponent>
<SubComponent1 /> // <- valid child
</SuperComponent>
)
}
class MyComponent extends React.Component {
render () {
return (
<SuperComponent>
<SubComponent2 /> // <- No! It's not right shape
</SuperComponent>
)
}
and the referenced SuperComponent and SubComponent1 are:
interface superPropsType = {
children: ReactElement<subPropsType1>
}
class SuperComponent extends React.Component<superPropsType> { ... }
interface subPropsType1 = {
name: string
}
class SubComponent1 extends React.Component<subPropsType1> { ... }
interface subPropsType2 = {
title: string
}
class SubComponent2 extends React.Component<subPropsType2> { ... }
I want SubComponent1 to be the only valid child of SuperComponent, that is, I wish typescript can throw an error if I place <SubComponent2 /> or Other types as child of <SuperComponent>
It seems like typescript only check that the child of should have the type of ReactElement, but ts doesn't check the shape of props of that child (which is subPropsType1), that is, if I place a string or number as child of SuperComponent, ts will complaints that type requirement doesn't meet, but if I place any jsx tag here(which will transpiled to ReactElement), ts will keep silent
Any idea ? And if any configs are required to post here, please don't hesitate to ask
Really appreciate any idea and solution
As of TypeScript 3.1, all JSX elements are hard-coded to have the JSX.Element type, so there's no way to accept certain JSX elements and not others. If you wanted that kind of checking, you would have to give up the JSX syntax, define your own element factory function that wraps React.createElement but returns different element types for different component types, and write calls to that factory function manually.
There is an open suggestion, which might be implemented as soon as TypeScript 3.2 (to be released in late November 2018), for TypeScript to assign types to JSX elements based on the actual return type of the factory function for the given component type. If that gets implemented, you'll be able to define your own factory function that wraps React.createElement and specify it with the jsxFactory compiler option, and you'll get the additional type information. Or maybe #types/react will even change so that React.createElement provides richer type information, if that can be done without harmful consequences to projects that don't care about the functionality; we'll have to wait and see.
I would probably declare SuperPropsType.children as:
children: React.ReactElement<SubPropsType1> | React.ReactElement<SubPropsType1>[];
To account for the possibility of having both a single and multiple children.
In any case, as pointed out already, that won't work as expected.
What you could do instead is declare a prop, let's say subComponentProps: SubPropsType1[], to pass down the props you need to create those SubComponent1s, rather than their JSX, and render them inside SuperComponent:
interface SuperPropsType {
children?: never;
subComponentProps?: SubPropsType1[];
}
...
const SuperComponent: React.FC<SuperPropsType> = ({ subComponentProps }) => {
return (
...
{ subComponentProps.map(props => <SubComponent1 key={ ... } { ...props } />) }
...
);
};

Generic components with higher order components

I'd like to give my React component props a generic type but this is being lost when I wrap it in a higher order component (material-ui) how do I pass along the required information?
type Props<T> = {
data: Array<T>;
}
class MyComponent<T> extends React.Component<Props<T>> {
const StyledComponent = withStyles(styles)(MyComponent)
Using <StyledComponent<myType gives an error as it doesn't know about the generic.
My bet is to annotate your new component like this:
const StyledComponent: <T>(props: OuterProps<T>) => JSX.Element = withStyles(styles)(MyComponent) ;
Mind you probably need to differentiate OuterProps and InnerProps, see below example I made for reference:
https://stackblitz.com/edit/hoc-generics
Hope that helps!

How to create stateless functional components that accept generics?

Typescript generics can used to extend interfaces.
interface Sample1<P> {
prop1: P;
}
interface Sample2<P> extends Sample1<P> {
prop2: string;
}
But when I try to create a functional component that uses the generic interface, typescript throws error.
const SampleSFC: React.SFC<Sample2<P>> = () => <div />;
error TS2304: Cannot find name 'P'.
If I replace P with a known type like string the error goes away.
const SampleSFC: React.SFC<Sample2<string>> = () => <div />;
Instead of hard coding type of P, that totally kills the purpose of generics, I want to enable users of SampleSFC to set type of P.
How can I do that? If it's not possible then what alternate design I should follow that would let me have SFC with generic props.
Generic type variables can be used in function and type declarations like your interfaces Sample1 and Sample2.
But when you actually use/call/invoke generic types, you have to specify a concrete type argument instead of P.
React.SFC - the stateless function component type - is an interface declaration, so using it in an const assignment requires a concrete type for P.
The only solution that comes to my mind is to make the SFC look like a function type for the ts-compiler, since function declarations/expressions can
be assigned generic type parameters. See Typescript docs for some examples.
Generic SFC declaration
Leave out the React.SFC type annotation to your assignment const SampleSFC:
const SampleSFC = <P extends {}>(props: Sample2<P>) => <div>{props.prop1} </div>;
Note: The <P extends {}> part is needed due to JSX/TSX compatibility
issues with generics, see the end of this post.
This way, your component remains a plain function. If you need the additional members from React.SFC, add them to your props. E.g. if children needed:
const SampleSFC = <P extends {}>(props: Sample2<P> & { children?: ReactNode })
=> <div >{props.prop1} </div>;
Generic SFC usage
Since Typescript 2.9, the type parameter can be directly set in JSX.
const MyApp: React.SFC<{}> = (props) => {
return <SampleSFC<number> prop2="myString" prop1={1} />
}
Alternatives
1.) A switch to class components would be easy, if you don't like the quirks.
class SampleSFCClass<P> extends React.Component<Sample2<P>> {
render() {
return <div>{this.props.prop1}</div>
}
}
const MyApp2: React.SFC<{}> = (props) => {
return <SampleSFCClass<number> prop2="myString" prop1={1} />
}
2.) You could even wrap the SFC in another function.
const withGeneric: <P>() => React.SFC<Sample2<P>> = <P extends {}>() => {
return (props) => <div> {props.prop1} {props.prop2} </div>;
}
const SampleSFCCalled: React.SFC<Sample2<string>> = withGeneric<string>();
const MyApp: React.SFC<{}> = (props) => {
return <SampleSFCCalled prop2="myString" prop1="aString" />
}
It will work, but disadvantage might be slight performance decrease, because the SFC funtion is always recreated in each render cycle of the parent comp.
JSX/TSX compatibility issues with Generics:
In some constellations Typescript seems to have problems parsing generics in combination with JSX/TSX (up to most recent 3.0.1) due to ambiguous syntax. Compile error will then be:
"JSX element has no corresponding closing tag."
One of the contributors recommended to use the function syntax in this case (see issue).
When you stick to arrow function, workaround is to let the type parameter extend from object (shown here or here) to clarify its meant to be a generic lambda and not a JSX tag:
<P extends {}> or <P extends object>
Hope, that helps.
Generics only work within generic types. So when you are declaring a type interface Sample1<P>, then you can use P as a type within that declaration. However, the concrete type Sample1<P> does not actually exist. Instead P is a placeholder for a type when the Sample1 type is being used.
So using Sample1<P> outside of a generic type definition that also has a generic type argument P does not really work. You can only use actual non-generic types P then.
Thatโ€™s why Sample1<string> works, because string is a valid value for the type argument P in the Sample1<P> definition.
If you want so have a SampleSFC that works for any type P, then you could create it as a React.SFC<Sample1<any>>:
const SampleSFC: React.SFC<Sample1<any>> = () => <div />;
interface MyComponentProps<T> {
value?: T;
}
export const MyComponent = <T,>(
props: React.PropsWithChildren<MyComponentProps<T>>,
) => {
return (
<View>
{props.children}
<Text>{(props.value as unknown) as string}</Text>
</View>
);
};
then in the render method..
return (
<MyComponent<string> />
)

Resources