Extends type of accepted props - reactjs

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

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.

Type is not assignable to type LibraryManagedAttributes

I am trying to create array field component that will accept any React Functional component that has BaseProps. However I get an error when rendering Component in ArrayField.
Please see code below. Any ideas what's wrong here?
type BaseProps<T> = {
name: string;
convertValue?: (value: T) => T;
};
type CompanyType = {
address: string;
employees: number;
};
type CompanyProps = BaseProps<CompanyType> & {
required?: boolean;
};
const Company = (props: CompanyProps) => {
return <div>{/** */}</div>;
};
type ArrayFieldProps<T, V extends React.FC<BaseProps<T>>> = {
Component: V;
componentProps: React.ComponentProps<V>;
values: T[];
};
const ArrayField = <T, V extends React.FC<BaseProps<T>>>({
Component,
values,
componentProps
}: ArrayFieldProps<T, V>) => {
return (
<React.Fragment>
{values.map((_, index) => (
<Component key={index} {...componentProps} />
))}
</React.Fragment>
);
};
export const App = () => {
const companies: CompanyType[] = [];
return (
<ArrayField
values={companies}
Component={Company}
componentProps={{
name: 'company',
convertValue: (value) => ({
...value,
address: value.address.toUpperCase()
}),
required: true
}}
/>
);
};
I would do it slightly differently. Component types are quite complex so IMO it's easier to reason about simpler types, and it this scenario it solves your problem. Instead of using the Component as a "base" for your interface, use props. Like this:
type BaseProps<T> = {
name: string;
convertValue?: (value: T) => T;
};
type ArrayFieldProps<T, P extends BaseProps<T>> = {
Component: React.ComponentType<P>;
componentProps: P;
values: T[];
};
const ArrayField = <T, P extends BaseProps<T>>({
Component,
values,
componentProps
}: ArrayFieldProps<T, P>) => {
return (
<>
{values.map((_, index) => (
<Component key={index} {...componentProps} />
))}
</>
);
};
So as you can see the main difference is that the second generic type has to extend BaseProps<T> instead of a component type with specific props (this is most likely where TypeScript gives up and it results in problems with key prop) and ultimately you want your Component to be any valid React component (whether it's class or a function one). Of course if you really insist on enforcing function components you can change React.ComponentType to React.FC and it would still work.
You have to take into consideration the
key={index}
portion because there is a mismatch from the expected type and what is passed. This portion is not included in any of the types and I guess typescript just interprets it as value to be passed (not actual key).
You may try to move it on outer div just to see if the situation improves.

Enforced properties on a React component with TypeScript

I would like to enforce properties on a React component with TypeScript, but I am getting weird behaviour. Bellow I am pasting only simple examples:
function FunctionalComponent(props: { color: string }) {
return <></>;
}
type ComponentWithName<I extends React.FunctionComponent<{ name: string } & React.ComponentProps<I>>> = I;
const component: ComponentWithName<typeof FunctionalComponent> = FunctionalComponent;
The code above will pass even I declared that the component must have a property name. With this code I need to get an errror, because the FunctionalComponent does not include a name property.
On the other hand, this works:
function FunctionalComponent(props: { color: string }) {
return <></>;
}
type ComponentWithName<I extends React.FunctionComponent<{ name: string }>> = I;
const component: ComponentWithName<typeof FunctionalComponent> = FunctionalComponent
This code will throw a TypeScript error, exactly what I need. But the issue is, that the FunctionalComponent can not have additional properties unless I add them manually to the React.FunctionComponent.
The goal is to enforce a component to have the "name" property, but allow to have more additional (not specified) properties.
I am using TypeScript version 4.4.4 and React version 17.0.2
Edit:
The true use case is this:
function Component<
I extends
| React.ComponentClass<
{
onChange: (event: React.ChangeEvent) => void;
} & React.ComponentProps<I>
>
| React.ComponentType<
{
onChange: (event: React.ChangeEvent) => void;
} & React.ComponentProps<I>
>
>(
props: {
component?: I;
} & Omit<React.ComponentProps<I>, "onChange">
) {
const { component: Component, ...rest } = props;
const handleChange = () => {
//
};
return (
<div>
{Component ? (
<Component
{...(rest as React.ComponentProps<I>)}
onChange={handleChange}
/>
) : (
<input onChange={handleChange} />
)}
</div>
);
}
class ComponentClass extends React.Component<{
color: "blue" | "yellow";
}> {
render() {
return (
<input style={{color: this.props.color}} />
);
}
}
function ComponentFunction(props: { color: "blue" | "yellow" }) {
return <input style={{color: props.color}} />;
}
function App() {
return (
<>
<Component component={ComponentClass} color="blue" />
<Component component={ComponentFunction} color="blue" />
</>
);
}
The <Component component={ComponentClass} color="blue" /> will throw an type error but the <Component component={ComponentFunction} color="blue" /> does not. I need to enforce passed components to have the onChange property with the specified type.
I may be missing something, but do you not just need to enforce the type of the Props rather than create a typed component?
interface NameProps {
name: string;
}
type NamedComponent<T extends NameProps> = (props: T) => JSX.Element;
const notANamedComponent: NamedComponent<{ int: number }> // ...this will give you an error
const aNamedComponent: NamedComponent<{ int: number; name: string}> //OK
The issue is that passing extra fields to the component is always valid, if you want to require it to do something with it is harder to type.
For instance this is valid code:
// from this context the argument will be called with an argument with both a and b properties
function takeF(f: (data: {a:string, b:number})=>any){}
// this function takes an object with an a property, but passing other properties would still be valid
function f(data: {a:string}){}
// this is allowed because passing an object with extra fields is still valid.
takeF(f)
The reason you are getting an error with {name: string} and {color:string} is because those have no overlap so typescript does give you errors, so the solution is to constrain your generic to what you actually need.
declare function Component<
ComponentProps extends { onChange: (event: React.ChangeEvent) => void; }
>(
props: {
component?: React.ComponentClass<ComponentProps> | React.ComponentType<ComponentProps>;
} & Omit<ComponentProps, "onChange">
): any
this way if the component doesn't have an onChange then there is no overlap and you get the error you are expecting and if there are extra properties it is fine because those are already being captured by the generic behaviour. Also note this is basically the same thing that #Marcus is saying, just constrain the generic to what you actually need.

React Typescript: FunctionComponent with templated props

I have a component with templated props:
const MyComponent = <Value extends any>({ value }: MyComponentProps<Value>) => <div />;
Which I can use without explicitly specifying the type of Value (it is inferred):
<MyComponent value="1" />
<MyComponent value={1} />
I usually write my components this way:
const MyComponent: FunctionComponent<MyComponentProps> = ({ value }) => <div />;
But haven't found a way to template MyComponent and MyComponentProps with this syntax... Does anyone know how to do it? Thanks in advance!
You can extend your ComponentProps like this:
interface MyComponentProps<T> {
value: T
}
const MyComponent: FunctionComponent<MyComponentProps<string>> = ({ value }) => <div />;
Now value is whatever you pass in <>, for example string or any.
If you want value to be any type:
type MyComponentProps = {
value: any
};
or just one of explicit types (a.k.a. union type):
type MyComponentProps = {
value: string | number
};
const MyComponent: FunctionComponent<MyComponentProps> = ({ value }) => <div />;
Based on your comment you just want this:
interface MyComponentProps<V> {
views: V;
initialView: keyof V;
}
type KeyedFunctionComponent<T> = FunctionComponent<MyComponentProps<T>>;
const MyComponent: KeyedFunctionComponent<Views> = (views, initialViews) => <div />;
Then declare your function component using either an interface or "typeof views" as the generic argument. Which is good I think. But, what you really want is this combined with a generator, which will allow you to bind and template correctly:
// Declare some views constant:
const views = { home: "home", index: "index" };
// Declare a type for our bound views component
interface KeyedProps<V> {
initialView?: keyof V;
}
// declare a type for our input function
interface KeyedWithViewsProps<V> extends KeyedProps<V> {
views: V;
}
// This is the binding function itself
function createKeyedComponent<T>(views: T, toWrap: FunctionComponent<KeyedWithViewsProps<T>>): FunctionComponent<KeyedProps<T>> {
return (props: KeyedProps<T>) => toWrap({views, initialView: props.initialView});
}
// Call the binder, and pass in the function we want to bind.
const MyComponent = createKeyedCompnonet(views, () => <div />);
// Now, using the component, only requires the optional initialView param, and it it type-checked to ensure it is a member of views
<MyComponent initialView="home" /> // works
<MyComponent initialView="other" /> // doesn't work
This is what I do
export type MyCompProps<ItemType> = {
item: ItemType;
};
export const MyComp = <ItemType>(props: MyCompProps<ItemType>): React.ReactElement => {
return <Text>item.toString</Text>;
}
Then I can just call
<MyComp item={myItem} />
And it figures it out.

React & Typescript component props type for `Component`

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;
}

Resources