TypeScript not inferring props from React.ComponentType - reactjs

I have the following function which I want to use to take in a ComponentType and its props in order to allow me to inject those props along with the RouteComponentProps
const routeComponentFactory = <TProps extends {}>(
Component: React.ComponentType<TProps>,
props: TProps
) => {
return (routeProps: RouteComponentProps) => {
return <Component {...routeProps} {...props} />;
};
};
This function works correctly if I explicitly specify TProps, for example:
interface MyComponentProps { a: number; b: number; }
const MyComponent: React.FunctionComponent<MyComponentProps> = () => null;
routeComponentFactory<MyComponentProps>(MyComponent, {});
I get an error there for not providing a and b in the object.
However, if I remove the explicit <MyComponentProps>, the error goes away and I am allowed to call the function with an empty object.
How can I make TypeScript properly infer MyComponentProps?

If you enable strictFunctionTypes you will get an error:
Type 'FunctionComponent<MyComponentProps>' is not assignable to type 'FunctionComponent<{}>'.
There are two possible inference sites for TProps the arguments Component and props both contain the type parameter TProps so typescript tries to find a type that will make both sites happy. Since if TProps were {} both the argument {} and the props type MyComponentProps would be assignable to it typescript infers TProps to be {} in order to keep everyone happy.
The reason that under strictFunctionTypes you do get an error is that by default function type behave bivariantly (so a function (p: MyComponentProps) => JSX.Element is assignable to (p: {}) => JSX.Element). Under strictFunctionTypes function types behave contravariantly so such an assignment is disallowed.
The solution to get an error even without strictFunctionTypes is to decrease the priority of the props inference site, so the compiler picks what is good for Component and checks it against props. This can be done using an intersection with {}:
const routeComponentFactory = <TProps extends {}>(
Component: React.ComponentType<TProps>,
props: TProps & {}
) => {
return (routeProps: any) => {
return <Component {...routeProps} {...props} />;
};
};
interface MyComponentProps { a: number; b: number; }
const MyComponent: React.FunctionComponent<MyComponentProps> = () => null;
routeComponentFactory(MyComponent, {}); // Argument of type '{}' is not assignable to parameter of type 'MyComponentProps'

I cannot see it allowing me to call the function with an empty object but in my case it is complaining that the MyComponent prop is invalid.
It seems the problem is it is inferring the type from the second parameter - I guess because it is matching on the simplest type.
You could define your function like this to force it to infer the correct type:
const routeComponentFactory = <TProps extends {}>(
Component: React.ComponentType<TProps>
) => (
props: TProps
) => {
return (routeProps: RouteComponentProps) => {
return <Component {...routeProps} {...props} />;
};
};
And then call it like this:
routeComponentFactory(MyComponent)({});
And it should complain about the empty object correctly in this case.

Related

Why Typescript Polymorphic React Components have wrong event handler props?

I simplified the code for readability.
interface OwnProps<T extends ElementType> {
as?: T;
}
type Props<T extends ElementType> = OwnProps<T> & ComponentPropsWithoutRef<T>;
const Component = <T extends ElementType = 'button'>({as, ...rest}: Props<T>) => {
const Tag = as || 'button';
return <Tag {...rest} />;
};
And when I'm trying to use Component I get a TS error:
TS7006: Parameter 'e' implicitly has an 'any' type.
57 |
> 58 | <Component onClick={(e) => e}/>;
| ^
59 |
Sounds weird, but any other props except event handlers work correctly.
When you create a function in TypeScript which uses a generic type parameter, you must specify the type parameter when invoking the function (unless it can be inferred from an argument).
A React function component is just a function, so when you invoke the function component in your JSX, you must still supply the generic type parameter in order for the compiler to have that type information.
In the code shown in the error message of your question, you don't do that, so the event type is unknown to the compiler:
const reactElement = <Component onClick={(e) => e}/>; /*
~
Parameter 'e' implicitly has an 'any' type.(7006) */
By supplying a valid generic type parameter for the element, the compiler will infer the event type:
const reactElement = <Component<'div'> onClick={(e) => e}/>; // Ok!
//^? (parameter) e: React.MouseEvent<HTMLDivElement, MouseEvent>
In the component you showed, the as prop should probably not be optional. When you supply the as prop, the compiler will infer the generic from its value:
const reactElement = <Component as="div" onClick={(e) => e}/>; // Ok!
//^? (parameter) e: React.MouseEvent<HTMLDivElement, MouseEvent>
Code in TypeScript Playground
If you must use a pattern of accepting the as prop optionally (with a default element type of "button" when it is not provided, you can use a function overload signature to make that clear to the compiler:
TS Playground
import {
default as React,
type ComponentPropsWithoutRef,
type ElementType,
type ReactElement,
} from 'react';
function Component <T extends ElementType>({as, ...rest}: { as: T } & ComponentPropsWithoutRef<T>): ReactElement;
function Component ({as, ...rest}: { as?: never } & ComponentPropsWithoutRef<'button'>): ReactElement;
function Component <T extends ElementType>({as, ...rest}: { as?: T } & ComponentPropsWithoutRef<T>) {
const Tag = as ?? 'button';
return <Tag {...rest} />;
}
const reactElement1 = <Component onClick={(e) => e}/>;
//^? (parameter) e: React.MouseEvent<HTMLButtonElement, MouseEvent>
const reactElement2 = <Component as="div" onClick={(e) => e}/>;
//^? (parameter) e: React.MouseEvent<HTMLDivElement, MouseEvent>

Typescript how to make a fucntion optional when its passed via props

I'm trying to pass a function through props but I'd like it to be optional. However, if I try to make it optional in the props interface by putting a "?" in front of the function, it gives me this error:
'propsFunction', which lacks return-type annotation, implicitly has an 'any' return type.
Is there any way to make a function optional to pass via props?
interface Props {
users: Array<User> | undefined;
propsFunction(userTo:string): void;
}
const Component: React.FC<Props> = (props) => {
You can use arrow function syntax to define the type
interface Props {
users: Array<User> | undefined;
propsFunction?: (userTo: string) => void;
}
const Component: React.FC<Props> = (props) => {

How to isolate known properties in an intersection of a generic type and a non-generic type

I have an HOC that takes a withPaper prop but does not pass it to the component it will render.
import React, { ComponentType, FC } from "react";
import { Paper } from "#material-ui/core";
interface WithOptionalPaperProps {
withPaper?: boolean;
}
export const withOptionalPaper = <Props extends object>() => (
Component: ComponentType<Props>
) => ({ withPaper, ...otherProps }: Props & WithOptionalPaperProps) => {
if (withPaper) {
return (
<Paper>
<Component {...otherProps as Props} />
</Paper>
);
}
return <Component {...otherProps as Props} />;
};
// Code below shows how the code above will be used.
interface NonPaperedComponentProps {
text: string;
className: string;
}
const NonPaperedComponent: FC<NonPaperedComponentProps> = props => {
return <h1 className={props.className}>{props.text}</h1>;
};
// Code will be used like an HOC.
// 'withPaper' prop can be optionally added to wrap the underlying component in 'Paper'
const OptionalPaperedComponent = withOptionalPaper<NonPaperedComponentProps>()(
NonPaperedComponent
);
// All props except 'withPaper' should be passed to 'NonPaperedComponent'
const renderedComponent = (
<OptionalPaperedComponent withPaper className="Hello" text="Hello There" />
);
I have removed the errors by type casting with otherProps as Props. Without them it produces the error 'Props' could be instantiated with a different subtype of constraint 'object'
https://codesandbox.io/s/gallant-shamir-z2098?file=/src/App.tsx:399-400
I would have assumed that since I have destructured and isolated the known properties from Props & WithOptionalPaperProps the types would look like this:
{
withPaper, // type 'WithOptionalPaperProps["withPaper"]'
...otherProps // type 'Props'
}
How do I make it that the Component the withOptionalPaper returns with a withPaper prop without passing it to its children but still passing all the other props?
This is a limitation in how de-structured rest objects are types. For a long type TS did not even allow de-structuring of generic type parameters. In version 3.2 the ability to use rest variables with generic type parameters was added (PR) but the rest variable is typed as Pick<T, Exclude<keyof T, "other" | "props">>, or equivalently Omit<T, "other" | "props">. The use of the conditional type Exclude will work fine for the consumers of this function if T is fully resolved (ie not a generic type parameter) but inside the function, typescript can't really reason about the type that contains the Exclude. This is just a limitation of how conditional types work. You are excluding from T, but since T is not known, ts will defer the evaluation of the conditional type. This means that T will not be assignable to Pick<T, Exclude<keyof T, "other" | "props">>
We can use a type assertion as you have, and this is what I have recommended in the past. Type assertions should be avoided, but they are there to help out when you (ie the developer) have more information than the compiler. This is one of those cases.
For a better workaround we could use a trick. While Omit<T, "props"> is not assignable to T it is assignable to itself. So we can type the component props as Props | Omit<Props, "withPaper">. Since Props and Omit<Props, "withPaper"> are essentially the same type, this will not matter much, but it will let the compiler assign the rest object to the component props.
export const withOptionalPaper = <Props extends object>(
Component: ComponentType<Props | Omit<Props & WithOptionalPaperProps, keyof WithOptionalPaperProps>>
) => ( {withPaper, ...otherProps }: Props & WithOptionalPaperProps) => {
if (withPaper) {
return (
<Paper>
<Component {...otherProps} />
</Paper>
);
}
return <Component {...otherProps } />;
};
Playground Link

Extends type of accepted props

I have a component that's root node can be overridden with the value of passed prop.
interface Props {
propA?: string
as?: React.ElementType
}
const OverridableComponent = (props: Props) => {
const { as: Tag = 'div', ...otherProps } = props
return <Tag {...props} />
}
And another component that will be used as as.
interface SomeComponentProps {
propB: string //<-- mandatory prop
children?: React.ReactNode
}
const SomeComponent = (props: SomeComponentProps) => {
return <div someProp={props.propB}>{props.children}</div>
}
So, the result that I would love to achieve is something like this
<OverridableComponent propA='optionalProp' as={SomeComponent} />
and I'd like OverridableComponent to merge props from SomeComponent.
<OverridableComponent propA='optionalProp' as={SomeComponent}
propB={/*required form SomeComponentProps */'someValue />
To implement this I tried to use generics.
type Props<T extends ElementType> = {
propA?: string
as?: T
} & React.ComponentProps<T>
it works for the case when as prop is passed, but if it's not, every passed prop even from='string' is allowed, because it's allowed on SVG attributes of React.ElementType.
Any idea how to improve this case?
Is it viable for you to pass the required React.ElementType explicitely as Generic type arguments in JSX? Doing that, you do not rely on compiler inference and can narrow the type properly.
Component:
const AppWithComponent = () => (
<OverridableComponent<typeof SomeComponent>
propA="pa"
propB="pb"
// className="ajsf" ; error
// from='string' ; error
as={SomeComponent}
/>
);
Intrinsic Element:
const AppWithIntrinsicElementExplicit = () => (
<OverridableComponent<"div">
propA="pa"
// propB="pb" ; error
className="yehaa"
// from='string' ; error
as="div" // you also can drop this attribute
/>
);
Codesandbox

What is the TypeScript return type of a React stateless component?

What would the return type be here?
const Foo
: () => // ???
= () => (
<div>
Foobar
</div>
)
StatelessComponent type mentioned in this answer has been deprecated because after introducing the Hooks API they are not always stateless.
A function component is of type React.FunctionComponent and it has an alias React.FC to keep things nice and short.
It has one required property, a function, which will return a ReactElement or null. It has a few optional properties, such as propTypes, contextTypes, defaultProps and displayName.
Here's an example:
const MyFunctionComponent: React.FC = (): ReactElement => {
return <div>Hello, I am a function component</div>
}
And here are the types from #types/react 16.8.24:
type FC<P = {}> = FunctionComponent<P>;
interface FunctionComponent<P = {}> {
(props: PropsWithChildren<P>, context?: any): ReactElement | null;
propTypes?: WeakValidationMap<P>;
contextTypes?: ValidationMap<any>;
defaultProps?: Partial<P>;
displayName?: string;
}
interface ISomeCoolInterface {
some: 'string';
cool: 'string';
props: 'string'
}
const SomeCoolComponent
: React.FC<ISomeCoolInterface>
= ({ some, cool, props }): JSX.Element => {
return <SomeCoolComponent>{some, cool, props}</SomeCoolComponent>
}
The important bit here being the return type JSX.Element
The correct return type here is ReactElement<P>, but a better option would be to use React.StatelessComponent<P> like this
const Foo
: React.StatelessComponent<{}>
= () => (
<div>
Foobar
</div>
)
If using the function keyword, the best return type appears to be JSX.Element | null.
For now our team is using JSXNode as shorthand, since these are the only two types that can be returned directly as a JSX result:
type JSXNode = JSX.Element | null;
Edit: looks like eventually React.ReactNode is the intended return type for JSX but it's currently not possible. (Reference)
Background:
None of the answers here seem to address the most common modern case - that you have a function returning an element. What type should this return?
function MyComponent(): SomeTypeHere {
return <>...</>;
}
The recommended way to hide the component is to return null, so it's not clear what clean return type that would be. Typing JSX.Element | null everywhere or even making a custom type like that seems it should be unnecessary given how universal this case is. ReactNode also doesn't work because undefined can't be returned as JSX.
Overall the best return type seems to be JSX.Element | null. That is the return type of the FC type which is used if you're not using the function keyword:
const MyComponent: FC = () => { <>...</> }
I would also add .SFC, which stands for Stateless Functional Component.
const Foo
: React.SFC<{}>
= () => (
<div>
Foobar
</div>
)
See https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts
Each JSX element is just syntactic sugar for calling React.createElement(component, props, ...children).
function createElement<P extends DOMAttributes<T>, T extends Element>(
type: string,
props?: ClassAttributes<T> & P,
...children: ReactNode[]): DOMElement<P, T>;
So it's DOMElement<P, T>

Resources