React & Typescript component props type for `Component` - reactjs

I have the following HOC in React:
`restricted` prop
const ConditionalSwitch = ({component: Component, showIfTrue, ...rest}) => (
showIfTrue
? <Component {...rest}/>
: null
);
How do I define the props so that Typescript will be happy with it?
{component: Component, showIfTrue, ...rest}
I tried
export interface CSProps {
component: any,
showIfTrue: boolean
}
How do I handle the ...rest here?

If you want to preserve type safety, you need to make ConditionalSwitch generic and have it infer the full props passed to the component based on the actual value of Component. This will ensure that the client of ConditionalSwitch will pass in all the required properties of the used component. To do this we use the approach described here:
const ConditionalSwitch = <C extends React.ComponentType<any>>({ Component, showIfTrue, ...rest}: { Component: C, showIfTrue: boolean} & React.ComponentProps<C> ) => (
showIfTrue
? <Component {...(rest as any) }/>
: null
);
function TestComp({ title, text}: {title: string, text: string}) {
return null!;
}
let e = <ConditionalSwitch Component={TestComp} showIfTrue={true} title="aa" text="aa" /> // title and text are checked
When passing the rest to the component we do need to use a type assertion because typescript is not able to figure out that { Component: C, showIfTrue: boolean} & React.ComponentProps<C> minus Component and showIfTrue is just React.ComponentProps<C> but you can't have it all :)

Try this:
export interface CSProps {
component: any;
showIfTrue: boolean;
[key: string]: any;
}

Related

Hide generic type prop on export

I have a small set of components in which a Wrapper is going to manipulate its children (therefore referred to as Components by injecting a prop into each of the children via cloneElement.
The gotcha here is that Component props are of a generic type. When I expose Component on the code, I don't want one of its props to be on the signature, because it will be automatically injected by the Wrapper component.
I have a concise example which shows what I mean:
types.ts
export type SomeObject = {
someKey: string;
};
type PropThatWillBeInjected<T extends SomeObject> = {
fn: (value: string) => T;
};
export type WannaBePropTypes = {
name: string;
};
export type PropTypes<T extends SomeObject> = PropThatWillBeInjected<T> &
WannaBePropTypes;
Important: PropTypes<T> is what Component expects, but as a programmer, I want WannaBePropTypes to be the signature of this component.
Moving on...
Component.tsx
function Component<T extends SomeObject>(props: PropTypes<T>) {
const { fn, name } = props;
const result = fn(name);
return <div>Hello, {result.someKey}</div>;
}
export default Component;
Wrapper.tsx
function Wrapper(props: { children: ReactNode }) {
const { children } = props;
return (
<div id="wrapper">
{React.Children.map(
children as ReactElement<PropTypes<SomeObject>>,
(child, index) =>
cloneElement(child, {
...child.props,
fn: (value: string) => ({
someKey: `${value}-${index}`,
}),
})
)}
</div>
);
}
export default Wrapper;
As expected, when I try to use these components as the following, the code works but the compiler complains:
<Wrapper>
<Component name="Alice" />
<Component name="Bob" />
</Wrapper>
Property 'fn' is missing in type '{ name: string; }' but required in type 'PropThatWillBeInjected'.(2741)
Is there a way to cast Component so I don't need to pass fn manually? I know there's a way when the prop types is not generic...
What I've tried:
Making fn optional: works, but this is not the solution I'm looking for;
Wrapping Component with another component and passing a noop to Component: works, but I don't want to create this unnecessary wrapper;
A playground with this sample code: StackBlitz
If I inderstand your problem correctly, you want to call Component as <Component name="Alice" /> and there should be some internal logic for two cases: when fn was passed and when not. If so, you can create unnecessary type (instead of unnecessary wrapper) which will be one of WannaBePropTypes or full props. This is like some combination of your try#1 and try#2:
type FullProps<T extends SomeObject> = PropThatWillBeInjected<T> & WannaBePropTypes;
type PropTypes<T extends SomeObject> = FullProps<T> | WannaBePropTypes;
So fn is optional until you define children as ReactElement<FullProps<SomeObject>> in Wrapper component. This is how to tackle with Typescript only.
BTW: maybe you can just pass array of WannaBePropTypes objects into Wrapper instead of children? This sounds better if <Component name="Alice" /> should do nothing by itself.

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

concise and readable syntax for props in a react component in typescript

So if you declare a React.FC , then you get to do a type declaration and thus get to pass it props :
const FuntionalComponent: React.FC<personType> = ({ id, nationality, name, familyName, age, selected }) =>
<div>
...directly html
</div>
export default FuntionalComponent;
But you cannot declare any methods or use hooks there (I have not found a way)
Then there's the React.Component type :
class Component extends React.Component<{}, {Stateproperty: string}>{
constructor(){
}
hook(){
}
method(){
}
render() {
return (
<div>
...html finally
</div>
)
}
}
export default component;
as you can see I can pass a state but not a props.
If I try something like this :
class Component extends React.Component<{propsProperty: Array}, {Stateproperty: string}>{
and then add my propsProperty to my html :
<Component propsProperty={thisArray} />
Yet, they error out with the following entry:
TS2322: Type '{ propsProperty: any; }' is not assignable to type
'IntrinsicAttributes & { children?: ReactNode; }'.   Property
'tankData' does not exist on type 'IntrinsicAttributes & { children?:
ReactNode; }'.
These tutorials seem to indicate that there is no other way to declare components:
https://riptutorial.com/reactjs/example/25321/declare-default-props-and-proptypes
https://medium.com/#cristi.nord/props-and-how-to-pass-props-to-components-in-react-part-1-b4c257381654
I found this article about TypeScript errors in React:
https://medium.com/innovation-and-technology/deciphering-typescripts-react-errors-8704cc9ef402, but it didn't have my issue.
I also tried this solution : https://stackoverflow.com/a/47395464/4770754. Even though it's clearly not the same issue, it seemed somewhat close, yet it didn't help.
React docs are not helpful since they ignore TypeScript and are not human-readable.
I need a way that's both concise, respects TypeScript and allows for both props and methods within the class body. Does this not exist at all?
But you cannot declare any methods or use hooks there (I have not found a way)
A good standard way of declaring a FC is:
type ComponentProps = {
id: string,
nationality: string,
...
}
const MyComponent: React.FC<ComponentProps> = ({
id,
nationality,
...rest
}: ComponentProps) => {
const someMethod = () => {
console.log('I am console logging');
}
return(
<div>
{/* HERE YOU WILL RENDER STUFF */}
</div>
)
}
Note that in the above I deconstruct the props on instantiation so that id, nationality can be leveraged directly in the component.
I don't think you need to worry too much about the syntax highlighting until you're familiar with the above.

Strictly typing React higher order component which consumes properties with TypeScript

I'm trying to type some higher order React components which are more complex than only injecting properties. These HOCs must be able to:
receive properties that are not (or only optionally) forwarded to the wrapped component
provide some new properties to the wrapped component
take additional arguments when they are created
From what I can tell, this means we must have three types:
The properties the HOC provides to the wrapped component
The properties the HOC uses, but which the wrapped component not necessarily cares about
The properties of the wrapped component that the HOC doesn't care about
I have the following code that works, but I can't find a way to avoid the any cast when passing the props on to the wrapped component:
import React, { ComponentType } from 'react';
// Props of the wrapped component without the props provided by the HOC
type StrictWrappedProps<WrappedProps, InjectedProps> = Omit<
WrappedProps,
keyof InjectedProps
>;
type WrappedComponent<WrappedProps, InjectedProps> = ComponentType<
WrappedProps & InjectedProps
>;
type HOCType<WrappedProps, InjectedProps, OwnProps> = ComponentType<
OwnProps & StrictWrappedProps<WrappedProps, InjectedProps>
>;
type LengthifyOwnProps = {
name: string;
};
type LengthifyInjectedProps = {
length: number;
};
export const lengthify = <WrappedProps extends {}>(
Wrapped: WrappedComponent<WrappedProps, LengthifyInjectedProps>,
multiplier: number
): HOCType<WrappedProps, LengthifyInjectedProps, LengthifyOwnProps> => ({
name,
...props
}) => {
const length = name.length * multiplier;
return <Wrapped {...props as any} length={length} />;
};
Here is an example usage of that HOC:
const myComp = ({ bool, length }: { bool: boolean; length: number }) => (
<p>
Bool is: {bool}. Length is: {length}
</p>
);
const EnhancedMyComp = lengthify(myComp, 2);
const usage = () => <EnhancedMyComp bool={true} name="Hello" />;
The any cast is a small workaround, but I would like to find the strict way of typing this.
The React TypeScript cheat sheet on HOCs links to this bug in the TypeScript repo, but it seems to be more or less solved, and only relates to HOCs that inject properties.
Other questions here on StackOverflow also seem to to be about injecting properties, or confusion about the ComponentType type.

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

Resources