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.
A stateless function component is a typical React component that is defined as a function that does not manage any state. There are no constructors needed, no classes to initialize, and no lifecycle hooks to worry about. These functions simply take props as an input and return JSX as an output.
Every React component that has a state influences its behavior (/render) or another component's behavior can be considered as a "stateful component". So for the function in the question - yes, App is stateful.
The setState() method rerenders the component, and you have a working stateful component.
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.
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>;
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} />
}
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With