Inferences from JSX vs calling React Components as functions - reactjs

I'm having a little trouble understanding how Typescript infers props in JSX. The issues has come up in the following context -- I'm trying to make a component that accepts another component as a prop, e.g. (a simplified example):
function WrapperComponent<T extends FooComponentType>(
{FooComponent}:{FooComponent:T}
){
return FooComponent({className:"my-added-class"})
}
Where FooComponentType is the type of a component that has a className prop:
type FooComponentType = (props:{className:string}) => JSX.Element
What I'd like, is for Typescript to disallow any attempt to pass in a component that doesn't have a className prop. Now, I recognize the typing above will not achieve that because React components are functions and so the props argument is covariant, i.e., a type that would be satisfied by a component with a className prop will also be satisfied by a component without one. Therefore, both of these components legitimately extend FooComponentType
const ComponentNoClass = ()=><div/>
const ComponentWClass = (props:{className:string})=><div/>
A workaround that I'm familiar with is to use a conditional time to essentially force the type to be bivariant by having some other type resolve to never if the passed component doesn't exactly match (i.e., if both types don't extend each other). My thought here was I could use such a type to make the return type of WrappedComponent be never if the passed component didn't have the className prop. So, to that end, I wrote the following helper type:
type JsxIfHasClassNameProps<
T extends FooComponentType
> = FooComponentType extends T ? JSX.Element : never
And used it to create a "smarter" version of WrappedComponent:
function CheckedWrapperComponent<T extends FooComponentType>(
{FooComponent}:{FooComponent:T}
):JsxIfHasClassNameProps<T>
{
return FooComponent({className:"my-added-class"}) as JsxIfHasClassNameProps<T>
}
I would think that would work, but it doesn't. Sort of... It doesn't work if I call CheckedWrapperComponent using JSX, but it does work if I call it as a normal function with arguments, namely:
// this type is JSX.Element as expected
const cjsx1 = <CheckedWrapperComponent FooComponent={ComponentWClass} />
// this type is also JSX.Element, but I want it to be never
const cjsx2 = <CheckedWrapperComponent FooComponent={ComponentNoClass} />
// this type is never as expected and desired
const cjsx3 = CheckedWrapperComponent({FooComponent:ComponentNoClass})
Can anyone explain what is going on here and how I can get the behavior I want?
Here is a sandbox link.
(BTW, I don't think this changes anything, but I had to call FooComponent as a function inside WrapperComponent to get around an error TS was giving me regarding the Intrinsic attributes it applies to JSX. I'm open to a better way to do that, but it's not a huge priority as that implementation is all hidden from the consumer. By contrast I can't reasonable expect consumers not to call CheckedWrapperComponent using JSX)
Edit
Just to clarify, my question is not about how to solve the contravariance issue generally. It's more specifically about how to solve it a way that is compatible with JSX. As noted in the second and third examples above:
// this type is also JSX.Element, but I want it to be never
const cjsx2 = <CheckedWrapperComponent FooComponent={ComponentNoClass} />
// this type is never as expected and desired
const cjsx3 = CheckedWrapperComponent({FooComponent:ComponentNoClass})
I can solve the issue if I pass in the component as a "normal" function argument. My question is why that stops working when I pass it in to JSX as a prop--and can this be achieved?
(Also, as noted in the comments, calling a functional component directly as is done in my examples is not the recommended way to do a non-JSX call, but I don't think that's relevant to my question which is really about TS. The same behavior occurs if you, properly, use React.createElement)

Check this out:
type FooComponentType = (props: { className: string }) => JSX.Element;
function wrapComponent<C extends FooComponentType extends C ? any : never>(
Wrapped: C
) {
return React.createElement(Wrapped, { className: "my-added-class" });
}
function ComponentNoClass(props: {}) {
const [state, setState] = React.useState(0);
return (
<div onClick={() => setState((x) => x + 1)}>
my classname is... oops I don't have a classname. [{state}]
</div>
);
}
function ComponentWClass(props: { className: string }) {
const [state, setState] = React.useState(0);
return (
<div onClick={() => setState((x) => x + 1)}>
my classname is {props.className}. [{state}]
</div>
);
}
function ComponentWClassPlus(props: { className: string; foo: number }) {
const [state, setState] = React.useState(0);
return (
<div onClick={() => setState((x) => x + 1)}>
my classname is {props.className}. [{state}]
</div>
);
}
export default function App() {
const [state, setState] = React.useState(0);
const jsx1 = wrapComponent(ComponentNoClass); // not fine
const jsx2 = wrapComponent(ComponentWClass); // fine
const jsx3 = wrapComponent(ComponentWClassPlus); // fine
return (
<div>
{jsx1}
{jsx2}
{jsx3}
<button onClick={() => setState((x) => x + 1)}>
increment outer count. [{state}]
</button>
</div>
);
}
What's going on here?
wrapComponent() is basically a "higher order component" function, that returns some rendered JSX from some component function/class that you call it with.
return React.createElement(Wrapped, { className: "my-added-class" });
The special sauce that gives you the type checking you crave is this: C extends FooComponentType extends C ? any : never.
It essentially says, "generic type C extends the conditional type (does FooComponentType extend the generic type C? If so, this type is any, otherwise it is never)".
It's a sort of counterintuitive way to get around the covariant issue you mentioned earlier, using a conditional type + never to assert the type. It feels circular somehow (C extends FooComponentType extends C???) but I guess because of the way TypeScript evaluates the conditional type from the inside out, it's fine?
You were pretty close with your implementation of CheckedWrapperComponent, but this way you cut out that middle man and only need to use the higher order component function.

Related

How to check whether a component renders a deeply nested child of a certain type?

A Modal component must render a Modal.Content, but not necessarily as its immediate child. For example:
Allowed
<Modal>
<Modal.Content>...</Modal.Content>
</Modal>
Also allowed
<Modal>
<UserForm>...</UserForm>
</Modal>
where UserForm renders a Modal.Content.
Not allowed
<Modal>
<UserForm>...</UserForm>
</Modal>
where UserForm doesn't render a Modal.Content.
I'd like to throw a warning if the user didn't use a Modal.Content inside a Modal.
How could I check whether a Modal renders a Modal.Content when the Modal.Content could be a deeply nested child?
Short answer, no, TS type system is structurally based, not nominally based. It is quite hard to do this in a clean and easy manner.
Longer answer
You can't do this very easily. With some imagination we can achieve this entirely through compile checks. We have to recursively go through the children nodes and check if it is our ModalContent, if we one leaf which satisfies this, then we can return JSX.Elemnt, otherwise return undefined as an indicator to the user that we are using Modal incorrectly
First of all is the biggest complication with JSX, is the implementation that the return type of all JSX components is React.ReactElement<any, any>, but we need the type alias generics to be typed so we can infer the values of the children.
Anytime we put something between <> the result will get inferred to React.ReactElement<any, any>.
I have tried to look around into overriding or declaration merging JSX.Element, to no success. Perhaps someone else may know how to overcome this so we can actually use JSX effectively.
So we'll have to throw out JSX entirely for this to work
Or you must be willing to throw out conventional JSX, we will have to assert certain types on the tree. Specifically only the branch components that lead to the ModalContent leaf/node has to be typed.
I've also switched to Function Components, it's the preferred way of doing React TSX nowadays, and reserve for some cases is almost always easier to type.
There's also limitations with this, I haven't really ensured if it works on Portalling/Fragments/or Lists
I'll describe the supported syntax first
// Works fine on shallow trees
<ModalFC>
<div></div>
<div></div>
<div></div>
{(<ModalContentFC />) as JSXCast<typeof ModalContentFC>} //Note the casting operator
</ModalFC>
// Have to use utility functions for deeper trees
// Have to cast using `as const` on any children lists (but only sometimes)
<ModalFC>
<div></div>
<div></div>
<div></div>
{Wrapper(MockDiv, {
children: (
[
<div></div>,
(<ModalContentFC />) as JSXCast<typeof ModalContentFC>
] as const //Necessary to enforce tuple
)
})}
</ModalFC>
//Not using JSX, a lot easier to type, but no more JSX :(
ModalFC({
children: Wrapper(MockDiv, {
children: Wrapper(MockDiv, {
children: Wrapper(MockDiv, {
children: Wrapper(MockDiv, {
children: ModalContentFC({})
})
})
})
})
})
The way this works is by recursing through the children key/value pair of the React Functions, and checking their children, and so on and so forth. We use conditional checking to decide what to do at each point.
type UnionForAny<T> = T extends never ? 'A' : 'B'
// Returns true if type is any, or false for any other type.
type IsStrictlyAny<T> =
UnionToIntersection<UnionForAny<T>> extends never ? true : false
type IsModalContentPresent<CurrentNode, Tag extends string> =
IsStrictlyAny<CurrentNode> extends true
? false
: CurrentNode extends Array<any>
? IsModalContentPresent<CurrentNode[number], Tag>
: CurrentNode extends Record<string, any>
? unknown extends CurrentNode['type']
// We aren't a ReactElement, check if we are a ReactPortal or ReactFragment
? unknown extends CurrentNode['children']
? CurrentNode extends Iterable<any>
// ReactFragment
? IsModalContentPresent<CurrentNode[keyof CurrentNode], Tag>
// I'm not sure if this would ever happen
: false
// ReactPortal
: IsModalContentPresent<CurrentNode['children'], Tag>
// ReactElement
: CurrentNode['type'] extends Tag
? true
: CurrentNode['props'] extends Record<string, any>
? IsModalContentPresent<CurrentNode['props']['children'], Tag>
: false
: false
function ModalFC<
C extends
// Default Component
ReactElement<P, T> | Readonly<ReactElement<P, T>> |
// List (I can't imagine supporting other JSX shapes)
Array<ReactElement<P, T>> | ReadonlyArray<ReactElement<P, T>>,
P = C extends Array<any>
? C[number]['props']
: C extends Record<string, any>
? C['props']
: never,
T extends string = C extends Array<any>
? C[number]['type']
: C extends Record<string, any>
? unknown extends C['type']
? C['defaultName']
: C['type']
: never,
>(props: ModalProps<C>):
Extract<IsModalContentPresent<C, 'Modal.Content'>, true> extends never ? undefined : JSX.Element
{
return null!
}
const ModalContentFC = (props: ContentProps): ReactElement<ContentProps, 'Modal.Content'> => null! //mock return
const Wrapper = <P extends {children: C}, C>(fc: (props: P) => JSX.Element, props: P): ReactElement<P, string> => fc(props)
View this all on TS Playground, I imagine (and know) this has a lot of limitations, and kind of requires you work very hard to know what you are doing with TS, if you want to fix all the edge-cases I probably built in. And to have a some understanding of how the maintainers at React decided to type the framework.
Supplementary reading:
Contextual Typing
Type Parameters
Conditional Types
Recursive Type References
When to use JSX.Element vs ReactNode vs ReactElement?
as const Assertions
as, or Type Assertions
Shorter Answer (Runtime-based)
It is a lot easier to check this at runtime than it is to add compile time support. This is because we don't have to care about types. I imagine the psuedocode is similiar, but instead of checking the "theoretical" tree of possible values, you can just recurse through the actual object tree. Perhaps use the key or some other unique value to mark the ModalContent then if it cannot find it in the deeply nested tree, throw an error when the component tries to mount.
I think this can be done pretty clean with Context, also works when you add another Modal in your Modal and ModelContent exists only in the inner Modal, but not in the outer one:
const ModalContext = createContext({ hasContent: false });
function CheckContentExists() {
const context = useContext(ModalContext);
if (!context.hasContent) {
throw new Error('You must use Modal.Content in Modal');
}
return null;
}
function Modal(props) {
return (<ModalContext.Provider value={{ hasContent: false }}>{props.children}<CheckContentExists/></ModalContext.Provider>);
}
function ThisWillUseModelContent() {
return (<ModalContent>Deep Modal.Content</ModalContent>)
}
function ModalContent(props) {
const context = useContext(ModalContext);
context.hasContent = true;
return props.children;
}
function NoError() {
return (
<Modal><ModalContent>Content</ModalContent></Modal>
);
}
function StillNoError() {
return (
<Modal><ThisWillUseModelContent>DeepContent</ThisWillUseModelContent></Modal>
);
}
function Error() {
return (
<Modal>
<div>Oops, no ModalContant:(</div>
</Modal>
);
}
function TrickyError() {
return (<Modal>
<div>No ModalContent in the first Modal, but we have ModalContent deeper in another Modal. It should still throw</div>
<Modal><ModalContent>This is OK, but the outer Modal is missing ModalContent:(</ModalContent></Modal>
</Modal>)}
You can even modify this to have exactly only one ModalContent in the Modal tree.
You can create a function with a recursive reduce to find a nested child Component by name:
function getChildComponentByName(children, componentName) {
const nodes = Array.isArray(children) ? children : [children];
return nodes.reduce((modalContent, node) => {
if (modalContent) return modalContent;
if (node) {
if (node.type && node.type.name === componentName) return node;
if (node.props) return getChildComponentByName(node.props.children, componentName);
}
}, null);
}
Then you can use that function in multiples places, one of them could be in propTypes definition. Example:
Modal.propTypes = {
children: function (props, propName, componentName) {
if (!getChildComponentByName(props.children, 'ModalContent')) {
return new Error(
'Invalid prop `' +
propName +
'` supplied to' +
' `' +
componentName +
'`. Validation failed. Modal Content is required as child element'
);
}
},
};
If ModalContent is not found as child component a warning will be shown in the JavaScript console.
Warning: Failed prop type: Invalid prop children supplied to Modal. Validation failed. Modal Content is required as child element
Modal
App
See working example
I didn't test all possible scenarios, but this could gives you a clue

React TypeScript accepts missing props on ComponentType prop

I'm trying to ensure that the component passed to another component as a prop (i.e. <Foo Component={Bar} />) is declared with the correct props.
This works fine when the component being passed in has props defined. However, if there are no props defined, TypeScript doesn't throw an error.
Here's an example:
import { ComponentType } from 'react';
type A = {
foo: string;
}
type B = {
Component: ComponentType<A>;
}
const Test = ({ Component }: B) => (
<Component foo="test" />
);
const NoProps = () => <div />;
const CorrectProps = ({ foo }) => <div />;
const IncorrectProps = ({ bar }) => <div />;
// does not error - but it should?
const C = () => <Test Component={NoProps} />;
// no error as expected
const D = () => <Test Component={CorrectProps} />;
// errors as expected
const E = () => <Test Component={IncorrectProps} />;
I was wondering if it is possible to enforce the correct props to be defined?
This is more a quirk of functions.
In javascript it raises no errors to provide more arguments than a function accepts. And typescript agrees.
For example:
type Fn = (arg: number) => number
const fn: Fn = () => 123 // this is fine
fn(999) // 123
See playground
This feature is really handy and used a lot. For example in a react onClick callback:
onClick={() => doStuff()} // no arg
onClick={(event) => doStuffWithEvent(event)} // with arg
That works because typescript allows you pass a function of fewer arguments to a function type expects more arguments. And it's pretty nice that we don't get an error in the first case where the argument is omitted. In general, it's better to omit the argument than to declare it and not use it.
So since functional components are just functions, the same rules apply. Remember that the <Component> angle brace syntax is just syntax sugar for calling your functions.
So:
<Component foo='test' />
Will end up calling:
Component({ foo: 'test' })
And if Component happens to be a function type that accepts no arguments, then no type error is raised because nothing bad will happen.
The reason that you do get an error here:
// errors as expected
const E = () => <Test Component={IncorrectProps} />;
Is that now you are passing in a function that takes one argument, and the type of that argument is actually wrong. This means your component requires a prop that Test will not pass to it, which will likely cause a crash.
So, in general, I don't think there's any harm in allowing you to pass a component that takes no props, and Typescript seems to agree.

typescript: Better way to type optional without default value?

I am writing react application with typescript.
To provide a typed props I am using the code below.
type ScheduleBoxContentProps = {
desc: ReactNode,
lottie: LottieProps,
} & Partial<{className: string}>;
As you can see, I want className prop to be optional, but don't want to define defaultProps for it. In addition, desc and lottie props should be provided.
Is there any better way to define optional without default ?
Edit
I am sorry for the missing context.
If I use React.FC with my custom prop type then there is no problem. Because React.FC uses Partial inside of it.
type MyProps = {
className?: string;
}
// no erros and warnings
const Component: React.FC<MyProps> = (props) => <div />;
But I don't want my component to accept the children props. I want my component to alert when children props are coming. Thus I am using function components with code below.
// error: default value for className is not provided.
const Component = ({className}: MyProps) => <div className={className} />;
and it tells me that className's default value are defined. I should define it explicitly. Using code below.
Component.defaultProps = {className: ''};
IMO, it seems little bit unnecessary code, so I decided to use Partial on optional props.
type MyProps = Partial<{className: string}>;
Is there any better way to achieve this? Or, using defaultProps is the best practice?
You can just add ? after the property name:
type ScheduleBoxContentProps = {
desc: ReactNode,
lottie: LottieProps,
// This ? is literally what Partial does
className?: string,
};
As for your edited example, what I think would be the most simple is to set the default value in the destructuring syntax:
const Component = ({className = ''}: MyProps) => <div className={className} />;
What about
className?: string;

Typescript with React - use HOC on a generic component class

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

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