React Component children typecheck with typescript - reactjs

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 } />) }
...
);
};

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

How to use the t() function in a class component with a constructor?

Using i18next with react, I would like to use the t() function in class components, but most of the time, I also need to initialize the state, so I would like to have a constructor as well.
Following the documentation here I get this:
import React from 'react';
import {WithTranslation, withTranslation} from "react-i18next";
interface ExampleProps extends WithTranslation {
}
interface ExampleState {
}
class Example extends React.Component<ExampleProps, ExampleState> {
render() {
const t = this.props.t;
return (
<div>
<p>{t('Test')}</p>
</div>
);
}
}
export default withTranslation()(Example);
This works fine, no problem here. Until I add a constructor:
constructor(props: ExampleProps, context: any) {
super(props, context);
}
withTranslation won't work with this:
TS2345: Argument of type 'typeof Example' is not assignable to parameter of type 'ComponentType<WithTranslationProps>'.   Type 'typeof Example' is not assignable to type 'ComponentClass<WithTranslationProps, any>'.     Types of parameters 'props' and 'props' are incompatible.       Type 'WithTranslationProps' is missing the following properties from type 'WithTranslation<"translation">': t, tReady
If I use interface ExampleProps extends WithTranslationProps {} the type incompatibility disappears, but I can't access the t function, because the WithTranslationProps only has a i18n? nullable type and I can't add anything to the ExampleProps, because I get Property 'newProperty' is missing in type 'WithTranslationProps' but required in type 'ExampleProps'.
const t1 = this.props.i18n?.t; results in TS2722: Cannot invoke an object which is possibly 'undefined' when trying to call t('Test').
const t2 = this.props.t; does not exists.
One solution, If I just initialize the state directly in the class, like:
class Example extends React.Component<ExampleProps, ExampleState> {
state = {
// set state here
}
render() {
Although I would still like to know if there is a way to use a constructor here.
From the React documentation:
If you don’t initialize state and you don’t bind methods, you don’t need to implement a constructor for your React component.
But, wait a minute, what is the context you passed to constructor?
you only need to pass props as the parameter of the constructor.
constructor(props)
In react constructor and super methods receive either 0 parameters or 1 (if it's 1, then they should be props), as per documentation
In your render you can just do
render () {
const { t } = this.props;
return (
<div>
<p>{t('Test')}</p>
</div>
);
}
This deconstruction of the components props makes your code more readable, if you need more variables from props, just add a comma and the name of the variable, and it will be usable inside that function.
Other than that your code seems fine

React component in TypeScript if passed as parameter and assigned to variable; TS2604: JSX element type does not have any construct or call signatures

I'm trying to hanlde props of passed React Element in Factory but I cannot because I receive typescript error:
TS2604: JSX element type 'this.extraBlock' does not have any construct or call signatures.
My Child component:
interface BlockTitleType {
title: string,
children?: React.ReactNode
}
const MainContactBlock: React.FC<BlockTitleType> = (e: BlockTitleType) => <div>{e.title}</div>;
My Parent component:
const factory = new TabBlockFactory(MainContactBlock);
const ContactBlocks: React.FC = () => <>factory.createBlock('test title')}</>
My factory:
interface BlockType {
title: string
}
class TabBlockFactory {
private mainBlock: React.ReactNode;
constructor(mainBloc: React.FC) {
this.mainBlock = mainBloc;
}
createBlock = ({title}: BlockType) => {
// the error occurs here:
// TS2604: JSX element type 'this.extraBlock' does not have any construct or call signatures.
return <this.mainBlock title={title}/>
}
}
it works only with any type, but it's an antipatern :(
Update:
I also tried types as React.Component, React.ReactElement, JSX.Elements
I've tried all 3 fixes from #Yuval:
Has no effect - renamed class variable this.mainBlock -> this.MainBlock;
Has no effect - introduced intermediate variable
const Component = this.mainBloc;
return <Component title={title} />;
Successfully helped me - private MainBlock: React.ComponentType<BlockTitleType>; as #Yuval proposed.
TLDR: working sandbox link
So there is a few problems with your code:
you are using the type React.ReactNode to represent a component, and it doesn't work. I recommend that to represent a React Component you use React.ComponentType<PROPS>.
So in your case in will be ComponentType<BlockTitleType>
the line <this.mainBlock /> is problematic, react doesn't like Components that do not start with an Upper case and also its an access to a this attribute at the same time, so separate that into 2 lines like so:
const Component = this.mainBloc;
return <Component title={title} />;
other than that I added some small syntax fixes and small improvements
quick assignment and declaration in the Factory class
missing { in ContactBlocks

TypeScript typeof type of generic class

I want to make a function which will return typeof React Component, which must implement a specific props interface.
I want to return a type, not an instance of that type.
So given this:
interface INameProps {
name: string;
}
interface ITypeProps {
type: string;
}
class Component1 extends React.Component<INameProps> {}
class Component2 extends React.Component<INameProps> {}
class Component3 extends React.Component<ITypeProps> {}
I would like to make a function, that could return components, for which the props interface extends INameProps
So far I figured this out:
export function getComponents<T extends INameProps, S extends any>(): React.Component<T, S> {
return Component1; // should be fine
return Component2; // should be fine
return Component3; // should not be allowed, since T for Component3 does not extend INameProps
}
But this is not correct - return type of this function is an instance of those components.
To get a type, I would think I would just have to add typeof keyword like so:
export function getComponents<T extends INameProps, S extends any>(): typeof React.Component<T, S>
But TypeScript does not like, that I add generics <T, S> after React.Component.
It compiles, when I define it like this:
export function getComponents<T extends INameProps, S extends any>(): typeof React.Component
But this does not do what I want - return type of function like this is a type of any React.Component.
How do I write this?
EDIT:
I went looking around, and found React.ComponentType (For Flow, I didn't see any documentation for TypeScript tho)
Turns out, the answer is rather simple. I was trying to come up with my own way using advanced types of TypeScript, but React already thought of this -
export function getComponent(): React.ComponentType<INameProps> {
return Component1; // allowed
return Component2; // allowed
return Component3; // not allowed, since props for Component3 does not extend INameProps
}
A constructor (or type) of some class C can be expressed as new () => C, plus-minus constructor args and generic types.
Still, your approach is not going to work here. When you have a generic function then it's the caller's decision to select its generic types. Sometimes they are inferred by the compiler, but anyway it's the caller that controls them. So someone might call your function as getComponent<SomeIrrelevantType>, and what will you want to return then? In runtime you are not even able to see that the generic type was set to something irrelevant.
The approach you can use instead is something similar to this:
export function getNamePropsComponents():
(new () => React.Component<INameProps, any>)[] {
return [Component1, Component2]; // should be fine
return [Component3]; // doesn't compile
}
I went looking around, and found React.ComponentType (For Flow, I didn't see any documentation for TypeScript tho)
Turns out, the answer is rather simple. I was trying to come up with my own way using advanced types of TypeScript, but React already thought of this -
export function getComponent(): React.ComponentType<INameProps> {
return Component1; // allowed
return Component2; // allowed
return Component3; // not allowed, since props for Component3 does not extend INameProps
}

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

Resources