I was asking this question with a more complex version of this basic concept
Rel: Can Generic JSX.Elements work in Typescript
I narrowed it down to the core Elements:
This is Object A that takes parameters from TypeA
type TypeA = {
label: string
value: number
}
const ObjA = ({ label, value }:TypeA) => {
return <div>
<div>Label: {label}</div>
<div>Value: {value}</div>
</div>
}
This is Object B that takes parameters from TypeB
type TypeB = {
label: string
value: string
bool: boolean
}
const ObjB = ({ label, value, bool }:TypeB) => {
return <div>
<div>Label: {label}</div>
{bool && <div>Value: {value}</div>}
</div>
}
Now I collect this ComponentGroup inside an array and create a Type out of this Array:
const ComponentCollection = [
ObjA,
ObjB
] as const
type Components = typeof ComponentCollection[number]
Then I create a generic component:
interface GenericProps<T extends Components> {
Component: T
title: string
}
const Generic = <T extends Components,>({ Component, title, ...props }:GenericProps<T>) => {
return (
<div>
<label>{title}</label>
<Component {...props}/>
</div>
)
}
At last I can call the generic component as follows:
<Generic Component={ObjA} title={'Usage A'} label={'Object A'} value={'String A'}/>
<Generic Component={ObjB} title={'Usage B no Bool'} label={'Object B'} value={0}/>
<Generic Component={ObjB} title={'Usage B with Bool'} label={'Object B'} value={0} bool/>
Altough it works really well in JavaScript, I messed something up with the typing.
I setup one TS-Playground and one Codepen:
TS-Playground: https://tsplay.dev/WvVarW
Codepen: https://codepen.io/Cascade8/pen/eYezGVV
Goal:
Convert this code above in correct TypeScript code
Compile without any TS-Errors or /#ts-ignore
Make IntelliSense work, so if you type <Generic Component={ObjA} ... it shows the available type attributes for this Object. In this case: label={string: } value={string: }
What i don't want:
Usage of classes or the old function syntax as our EsLint requires us to use an Arrow-Function if possible.
Passing the Objects as a Child.
I know this works, but it is not the prefered solution as the main Project has a lot of groups like this that get rendered like this.
And why shouldn't something work in TypeScript that works very simple in JavaScript.
Following the way you've constructed your types, you can do it using the definition of your generic inside GenericProps
(Note, I have bundled the props into a new props prop, as this should avoid name collisions should you have naming collisions)
import React from 'React'
type TypeA = {
label: string
value: number
}
const ObjA = ({ label, value }:TypeA) => {
return <div>
<label>{label}</label>
<label>{value}</label>
</div>
}
type TypeB = {
label: string
value: string
bool: boolean
}
const ObjB = ({ label, value, bool }:TypeB) => {
return <div>
<label>{label}</label>
{bool && <label>{value}</label>}
</div>
}
type Components = typeof ObjA | typeof ObjB;
interface GenericProps<T extends (...args: any) => any> {
Component: T
title: string
props: Parameters<T>[0]
}
const Generic = <T extends Components,>({ Component, title, props }:GenericProps<T>) => {
return (
<div>
<label>{title}</label>
<Component {...props as any}/>
</div>
)
}
const Usage = () => {
return <Generic Component={ObjA} title={'Usage'} props={{label: 'ObjectA'}}/>
}
export default Generic
It is possible to do without any kind of type assertions.
Consider this exmaple:
import React, { FC, } from 'react'
type Type = {
label: string;
value: string | number
}
type TypeA = {
label: string
value: number
}
type FixSubtyping<T> = Omit<T, 'title'>
const ObjA = ({ label, value }: FixSubtyping<TypeA>) => {
return <div>
<label>{label}</label>
<label>{value}</label>
</div>
}
type TypeB = {
label: string
value: string
bool: boolean
}
const ObjB = ({ label, value, bool }: FixSubtyping<TypeB>) => {
return <div>
<label>{label}</label>
{bool && <label>{value}</label>}
</div>
}
type Props<T> = T & {
title: string
}
const withTitle = <T extends Type>(Component: FC<FixSubtyping<Props<T>>>) =>
({ title, ...props }: Props<T>) => (
<div>
<label>{title}</label>
<Component {...props} />
</div>
)
const GenericA = withTitle(ObjA)
const GenericB = withTitle(ObjB)
const jsxA = <GenericA title={'Usage'} label={'A'} value={42} /> // // ok
const jsxB = <GenericB title={'Usage'} label={'A'} value={'str'} bool={true} /> // // ok
Playground
This error occurs because props is infered as Omit<T,'title'> so we should assure TS that Component props is compatible with Omit<T,'title'>
However there is a drawback, you need to update props type in other components. If it is not an option, I think the best approach would be to overload your Generic function:
import React, { FC, } from 'react'
type TypeA = {
label: string
value: number
}
type FixSubtyping<T> = Omit<T, 'title'>
const ObjA = ({ label, value }: TypeA) => {
return <div>
<label>{label}</label>
<label>{value}</label>
</div>
}
type TypeB = {
label: string
value: string
bool: boolean
}
const ObjB = ({ label, value, bool }: TypeB) => {
return <div>
<label>{label}</label>
{bool && <label>{value}</label>}
</div>
}
type Props<T> = T & {
Component: FC<T>,
title: string
}
function Generic<T,>(props: Props<T>): JSX.Element
function Generic({ Component, title, ...props }: Props<unknown>) {
return (
<div>
<label>{title}</label>
<Component {...props} />
</div>
)
}
const jsxA = <Generic Component={ObjA} title={'Usage'} label={'A'} value={42} /> // // ok
const jsxB = <Generic Component={ObjB} title={'Usage'} label={'A'} value={'str'} bool={true} /> // // ok
Playground
Related
I have a React component like this
import React from 'react';
import { ECOTileSummary } from './ECOTileSummary';
import { TileSummary } from './TileSummary';
interface SuperTileSummaryProps {
date?: string;
location: string;
link: string;
title: string;
nextVisit?: string | null;
tileEntity?: string;
}
export const SuperTileSummary = ({
date,
location,
link,
title,
nextVisit,
tileEntity,
}: SuperTileSummaryProps) => {
const chooseRegularTileByEntity = () => {
switch (tileEntity && tileEntity) {
case 'EmailCampaign':
return <ECOTileSummary date={date} link={link} location={location} title={title} />;
default:
return (
<TileSummary
nextVisit={nextVisit}
date={date}
link={link}
location={location}
title={title}
/>
);
}
};
return chooseRegularTileByEntity;
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
and I am calling it inside another component like this
import { SuperTileSummary} from './SuperTileSummary'
export const Wrapper = () => {
return(
<div>
<SuperTileSummary
nextVisit={nextVisit}
date={date}
link={link}
location={location}
title={title}
tileEntity={tileEntity}
/>
</div>
);
};
and I am getting an error like this: 'SuperTileSummary' cannot be used as a JSX component.
Its return type '() => JSX.Element' is not a valid JSX element.
Type '() => Element' is missing the following properties from type 'Element': type, props, keyts(2786)'.
I am not sure what am I doing wrong here since I'm rendering the component.
You can call chooseRegularTileByEntity function to get the returned value:
return chooseRegularTileByEntity();
or use it as component:
const ChooseRegularTileByEntity = () => {
//...
};
return <ChooseRegularTileByEntity />
The error message, 'SuperTileSummary' cannot be used as a JSX component. Its return type '() => JSX.Element' is not a valid JSX element., suggests that you're giving back a function that returns a JSX element where it expects a JSX element itself.
I believe the fix is to change return chooseRegularTileByEntity; to return chooseRegularTileByEntity(); The former is the function that returns JSX, the latter is the returned JSX itself
I'm totally new to using Typescript and having problems rendering a component but also passing in an onClick function. How can I pass in an onClick function to the CarItem Like this? I think it's trying to treat the onMenuClick as a proeprty of ICar but it isn't and shouldn't be. The onMenuClick shouldn't be part of the ICar interface. It is merely a function. I've put a simple example below.
interface ICar {
name: string,
colour: string,
manufacturer: string
}
const CarGrid : React.FC<ICarGridProps> = car => {
return (
<CarItem {...car} onMenuClick={onClick} />
)
}
const CarItem : React.FC<ICar> = (car, onMenuClick) => {
return (
<div onClick={() => onMenuClick(car.name)}>{car.name}</div>
);
}
Thanks all.
There are a few things wrong here:
I've corrected the return of CarGrid and type def and params of CarItem
interface ICar {
name: string,
colour: string,
manufacturer: string
}
const CarGrid : React.FC<ICarGridProps> = car => {
return (
<CarItem car={car} onMenuClick={onClick} />
)
}
const CarItem : React.FC<{car: ICar, onMenuClick: Function}> = ({car, onMenuClick}) => {
return (
<div onClick={() => onMenuClick(car.name)}>{car.name}</div>
);
}
Or if you want to spread the car object into CarItem, it should be refactored:
...
<CarItem {...car} onMenuClick={onClick} />
...
const CarItem : React.FC<ICar&{onMenuClick: Function}> = ({name, onMenuClick}) => {
return (
<div onClick={() => onMenuClick(name)}>{name}</div>
);
}
I have a React Wrapper Component, that accepts some props, but forwards all others to the child component (especially relevent for native props like className, id, etc.).
Typescript complains, however, when I pass native props. See error message:
TS2339: Property 'className' does not exist on type
'IntrinsicAttributes & IntrinsicClassAttributes< Wrapper > & Readonly< {
children?: ReactNode; }> & Readonly< WrapperProps>'.
How can I get a component with specific props that also accepts native props (without accepting any props and giving up on type checking)?
My code looks like this:
interface WrapperProps extends JSX.IntrinsicAttributes {
callback?: Function
}
export class Wrapper extends React.Component<WrapperProps>{
render() {
const { callback, children, ...rest } = this.props;
return <div {...rest}>
{children}
</div>;
}
}
export const Test = () => {
return <Wrapper className="test">Hi there</Wrapper>
}
FYI: I found a similar question here, but the answer basically gives up type checking, which I want to avoid: Link to SO-Question
We can have a look at how div props are defined:
interface IntrinsicElements {
div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
}
If we use React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> as the base type we will have all properties of div. Since DetailedHTMLProps just adds ref to React.HTMLAttributes<HTMLDivElement> we can use just this as the base interface to get all div properties:
interface WrapperProps extends React.HTMLAttributes<HTMLDivElement> {
callback?: Function
}
export class Wrapper extends React.Component<WrapperProps>{
render() {
const { callback, children, ...rest } = this.props;
return <div {...rest}>
{children}
</div>;
}
}
export const Test = () => {
return <Wrapper className="test">Hi there</Wrapper> // works now
}
JSX.IntrinsicElements has this info, e.g.
const FooButton: React.FC<JSX.IntrinsicElements['button']> = props => (
<button {...props} className={`foo ${props.className}`} />
)
// alternative...
const FooButton: React.FC<React.PropsWithoutRef<
JSX.IntrinsicElements['button']
>> = props => <button {...props} className={`foo ${props.className}`} />
discovered this in the react-typescript-cheatsheet project.
Have a look at ComponentProps, ComponentPropsWithRef, and ComponentPropsWithoutRef - this will accept a generic input that can be "div", "button", or any other component. It will include react specific props such as className as well:
import React, {
forwardRef,
ComponentPropsWithoutRef,
ComponentProps,
ComponentPropsWithRef
} from "react";
const ExampleDivComponent = forwardRef<
HTMLDivElement,
ComponentPropsWithoutRef<"div">
>(({ children, ...props }, ref) => {
return (
<div {...props} ref={ref}>
{children}
</div>
);
});
<ExampleDivComponent
className=""
style={{ background: "green" }}
tabIndex={0}
onTouchStart={() => alert("touched")}
/>;
const ExampleButtonComponent: React.FC<ComponentProps<"button">> = ({
children,
...props
}) => {
return <button {...props}>{children}</button>;
};
<ExampleButtonComponent onClick={() => alert("clicked")} />;
A co-worker of mine figured it out. Sharing here for broader visibility:
interface ComponentPropTypes = {
elementName?: keyof JSX.IntrinsicElements; // list of all native DOM components
...
}
// Function component
function Component({
elementName: Component = 'div',
...rest,
// React.HTMLAttributes<HTMLOrSVGElement>) provides all possible native DOM attributes
}: ComponentPropTypes & React.HTMLAttributes<HTMLOrSVGElement>)): JSX.Element {
return <Component {...rest} />;
}
// Class component
class Component extends React.Component<ComponentPropTypes & React.HTMLAttributes<HTMLOrSVGElement>> {
render() {
const {
elementName: Component,
...rest,
} = this.props;
return <Component {...rest} />
}
}
I have two types of chat messages in my chat application
export const UserType = "USER_MESSAGE";
export const SystemType = "SYSTEM_MESSAGE";
export type ChatType = typeof UserType | typeof SystemType;
I use a switch case to determine which type of message I am processing and depending on type choose to render its content.
This is where I run into trouble because depending on the type of the message, it will contain different content object structures.
const ChatMessages = ({ messages, style }: Input) => {
// FLOW ERROR: Property `text`: Property not found in object type
const renderUserMessage = (content: UserContent, i: number) => {
return <p key={`message-${i}`}>
<b>{content.userName}: </b>{content.text}
</p>;
};
// FLOW ERROR: Property `info`: Property not found in object type
const renderSystemMessage = (content: SystemContent, i: number) => {
return <p key={`message-${i}`}>
{content.info}
</p>;
};
return (
<div style={style}>
{messages.map((message, i) => {
switch (message.type) {
case UserType:
return renderUserMessage(message.content, i);
case SystemType:
return renderSystemMessage(message.content, i);
default:
}
})}
</div>
);
};
The rest of my types look like this
export type UserContent = {
userName: string,
text: string,
};
export type SystemContent = {
info: string,
};
export type ChatContent =
UserContent |
SystemContent;
export type MessageType = {
type: ChatType,
content: ChatContent,
};
Changing the type definition from UserContent and SystemContent to a generic Object solves the problem, but that's not as strict as I'd like it to be.
This problem is known issue. There is a solution (or work around) to simply reassign message.type to some variable before Switch statment, or to swap the switch with an appropriate if chain.
So in your case this should work:
return (
<div style={style}>
{messages.map((message, i) => {
let messageType = message.type;
switch (messageType) {
case UserType:
return renderUserMessage(message.content, i);
case SystemType:
return renderSystemMessage(message.content, i);
default:
}
})}
</div>
);
If I have something like this
const RandomComponent = (props) => (
<div>
<SomeSubComponent id={props.id} />
<AnotherSubComponent type={props.type} />
</div>
)
how will I type annotate the return type with Flow, i.e., what should replace /* ??? */ in the code below?
const RandomComponent = (props: { id: string, vino: number): /* ??? */ => (
<div>
<SomeSubComponent id={props.id} />
<AnotherSubComponent veryImportantNumber={props.vino} />
</div>
)
Edit: This is what the Flow docs has to say about stateless functional components. I may be blind, but I can't see anything about a return type there, only prop types.
The return type of a pure component (which is the same type of the render function of a normal component) is ?React$Element<any>.
As you can read in its definition React$Element has a type parameter Config which is not very useful per se and it's there only for consistency with the definition of ReactClass.
So your definition can be written as
const RandomComponent = (props: { id: string, vino: number }): React$Element<any> => (
<div>
<SomeSubComponent id={props.id} />
<AnotherSubComponent veryImportantNumber={props.vino} />
</div>
)
or if you prefer
import type { Element } from 'react'
const RandomComponent = (props: { id: string, vino: number }): Element<any> => (
<div>
<SomeSubComponent id={props.id} />
<AnotherSubComponent veryImportantNumber={props.vino} />
</div>
)
or even
import React from 'react'
const RandomComponent = (props: { id: string, vino: number }): React.Element<any> => (
<div>
<SomeSubComponent id={props.id} />
<AnotherSubComponent veryImportantNumber={props.vino} />
</div>
)
Turns out it's React.Element, which is a polymorphic type (which I'm not 100% sure what it means), so the correct (enough) code would be
const RandomComponent = (props: { id: string, vino: number): React.Element<*> => (
<div>
<SomeSubComponent id={props.id} />
<AnotherSubComponent veryImportantNumber={props.vino} />
</div>
)
Depending on your .flowconfig, setting React$Element<any> as the return type may throw the following error:
error Unexpected use of weak type "any" flowtype/no-weak-types
To avoid this, either pass no type at all:
type PropsType = { foo: string }
const Baz = (props: PropsType): React$Element =>
<h1>Hello, { props.foo }</h1>
Or, pass a props type alias, instead of any:
type PropsType = { foo: string }
const Baz = (props: PropsType): React$Element<PropsType> =>
<h1>Hello, { props.foo }</h1>