React TypeScript - Pass a dynamic generic type into a forwardRef component - reactjs

Core of my question
const FinalComponent<GenericType extends 'a' | 'b'> = is invalid tsx syntax.
// The 1st line here is invalid tsx syntax
const FinalComponent<InvalidGenericType extends 'a' | 'b'> =
forwardRef<HTMLParagraphElement, PropsWithStandardRef<InvalidGenericType>>(({ value }, ref) => {
return <Component forwardedRef={ref} value={value} />
}) as ComponentType<InvalidGenericType>
Intended usage of the component:
const ExampleUsage = () => <FinalComponent<'b'> value="b" />
How do I make a generic type in this situation?
Additional context
For additional context, here is the rest of the code:
import { Ref, forwardRef } from 'react'
// These are the base props for the component.
// In terms of usage, these are the props that I care about.
interface Props<GenericType extends 'a' | 'b'> {
value: GenericType
}
// Adding forwardedRef to the props to define what props are usable inside the component
interface PropsWithForwardRef<GenericType extends 'a' | 'b'> extends Props<GenericType> {
forwardedRef: Ref<HTMLParagraphElement | null>
}
// Adding standard ref to the props to define what props the component can accept from outside
interface PropsWithStandardRef<GenericType extends 'a' | 'b'> extends Props<GenericType> {
ref?: Ref<HTMLParagraphElement | null>
}
// forwardRef is interfering with the inheritance of the generic types.
// This is a stand in for the expected return type of the component.
type ComponentType<GenericType extends 'a' | 'b'> = (props: PropsWithStandardRef<GenericType>) => JSX.Element
// The core component code
function CoreComponent<GenericType extends 'a' | 'b'> ({ value, forwardedRef }:PropsWithForwardRef<GenericType>):JSX.Element {
return <p ref={forwardedRef}>{value}</p>
}
// !!!!!!!!!!! IMPORTANT BIT !!!!!!!!!!!!
// This is where my problem is, I need to be able to pass a dynamic generic type into PropsWithStandardRef and ComponentType.
// I'm not sure how to do that though because `const FinalComponent<InvalidGenericType extends 'a' | 'b'> = forwardRef()` is invalid
const FinalComponent<InvalidGenericType extends 'a' | 'b'> = forwardRef<HTMLParagraphElement, PropsWithStandardRef<InvalidGenericType>>(({ value }, ref) => {
return <CoreComponent forwardedRef={ref} value={value} />
// I need the `as ComponentType<InvalidGenericType>` bit because the inferred type that comes out of forwardRef
// is making TS lose the generic types information
}) as ComponentType<InvalidGenericType>
// This is the end goal of how I want to be able to use this component
// I want to be able to pass a generic type into the component without TS complaining
const ExampleUsage = () => <FinalComponent<'b'> value="b" />
PS. I recognize that this example is a bit contrived, it is for the sake of simplifying my real world problem which features a far more complex component.
Similar but different question
This is different to React Typescript - dynamic types
In that question, it doesn't require passing the type information into the variable, more just changing what the type is based on what values the user provides.
I need the end use of the component to be able to pass a type into it.

Your provided code is very close to working, but there are a just a few things which need to change for type correctness:
Refs which hold HTML elements should not be mutable/nullable, because they are set and managed by React, and the Ref<T> util already includes null anyway. (This is unless you're doing something truly exotic like imperatively manipulating elements outside the render tree... but I've never even seen that in a codebase.) Because of this, I removed null from your union in Ref<HTMLParagraphElement | null>. (This was also causing a problem with passing the value to the actual paragraph element in CoreComponent.)
The return type for your ComponentType needs to include null for the return type of forwardRef to be assignable to it. Speaking of return types for functions which return React elements, JSX.Element is simply an alias to ReactElement with any passed in the type params. I changed the JSX.Element references to ReactElement.
A type annotation can still be applied to a variable that holds a function expression value. It is written the same way any other annotation is written: following the identifier name, like this:
const add: (...numbers: number[]) => number = (...nums) => nums.reduce((sum, n) => sum + n, 0);
The syntax above is not easy to read in my opinion, so I prefer wrapping the type in parentheses for readability. You can even utilize generics with the signature (needed in your case), and overloading is possible as well. See the FinalComponent in your code below, modified:
TS Playground link
import {
default as React,
createRef,
forwardRef,
ReactElement,
Ref,
} from 'react';
type AorB = 'a' | 'b';
type Props<T extends AorB> = { value: T };
type PropsWithForwardRef<T extends AorB> = Props<T> & { forwardedRef: Ref<HTMLParagraphElement> };
type PropsWithStandardRef<T extends AorB> = Props<T> & { ref?: Ref<HTMLParagraphElement> };
function CoreComponent<T extends AorB> ({ value, forwardedRef }:PropsWithForwardRef<T>): ReactElement {
return <p ref={forwardedRef}>{value}</p>;
}
const FinalComponent: (<T extends AorB>(props: PropsWithStandardRef<T>) => ReactElement | null) =
forwardRef<HTMLParagraphElement, Props<AorB>>(({ value }, ref) => <CoreComponent forwardedRef={ref} value={value} />);
/**
* The annotation for the function expression above can also be written this way,
* which allows for overloading with multiple signatures, one on each line inside the braces:
*/
// const FinalComponent: {
// <T extends AorB>(props: PropsWithStandardRef<T>): ReactElement | null;
// } = forwardRef<HTMLParagraphElement, Props<AorB>>(({ value }, ref) => <CoreComponent forwardedRef={ref} value={value} />);
/* Use: */
const ref = createRef<HTMLParagraphElement>();
const ExampleA = () => <FinalComponent<'a'> value="a" ref={ref} />;
const ExampleB = () => <FinalComponent<'b'> value="b" ref={ref} />;
const RefOptional = () => <FinalComponent<'a'> value="a" />;
const NoRestrictionA = () => <FinalComponent value="a" ref={ref} />;
const NoRestrictionB = () => <FinalComponent value="b" />;
const InvalidA = () => <FinalComponent<'a'> value="b" ref={ref} />;
const InvalidNotAorB = () => <FinalComponent value="c" />;
const InvalidNoValue = () => <FinalComponent<'a'> />;

Variable can't use generic. define it as FC:
const FinalComponent: <InvalidGenericType extends 'a' | 'b'>(
props: PropsWithStandardRef<InvalidGenericType>
) => JSX.Element = forwardRef<
HTMLParagraphElement,
PropsWithStandardRef<'a' | 'b'>
>(({ value }, ref) => {
return <CoreComponent forwardedRef={ref} value={value} />
}) as ComponentType<'a' | 'b'>
Or use interface to define it:
interface FinalComponentType
extends React.ForwardRefExoticComponent<PropsWithStandardRef<'a' | 'b'>> {
<GenericType extends 'a' | 'b'>(
props: PropsWithStandardRef<GenericType>
): JSX.Element
}
const FinalComponent = forwardRef<
HTMLParagraphElement,
PropsWithStandardRef<'a' | 'b'>
>(({ value }, ref) => {
return <CoreComponent forwardedRef={ref} value={value} />
}) as FinalComponentType
Works fine:
const ExampleUsage = () => <FinalComponent<'a'> value="b" /> // There will be error

I've encountered this problem a number of times and while this is not a canonical answer this is how I usually resolve it:
You basically need your react function component to accept a generic type as the type parameter. The only way I found to deal with this was to create the function component without explicitly declaring it as a function component. The problem is:
const FinalComponent : React.FC<Props<GenericType<...>> = // There's no type variable that you can use at that point
To bypass this what I do is:
const FinalComponent = <T extends 'a'|'b'>(props: Props<T>): ReturnType<React.FC<Props<T>>> => null; //Return whatever you need here
This will declare a generic react function component that you can use like below:
const Res = () => <FinalComponent<'a'> value='a' />; // Works
const Res2 = () => <FinalComponent<'a'> value='b' />; // Errors
Random link

Related

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.

How to make a functional React component with generic type?

I'm trying to make a React component which takes in a generic type parameter which will be part of its prop type.
I want a solution that would look something like this:
interface TestProps<T> {
value: T;
onChange: (newValue: T) => void;
}
const Test: React.FC<TestProps<T>> = (props) => (
<span>{props.value}</span>
);
I have seen that there is support for this in TypeScript 2.9 and I'm on 4.3.5.
Usage of this would look like this:
const Usage: React.FC = () => (
<div>
<Test<Obj>
value={{ name: 'test' }}
onChange={(newValue) => {
console.log(newValue.name);
}}
/>
</div>
);
Code sandbox: https://codesandbox.io/s/react-typescript-playground-forked-8hu13?file=/src/index.tsx
You need to rewrite your Test component in this way
const Test= <T,>(props:TestProps<T>) => (
<span>Some component logic</span>
);
Can you show the same with React.FC<TestProps>?
It is impossible to do with FC.
This is FC implementation:
interface FunctionComponent<P = {}> {
(props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
// ... other static properties
}
As you might have noticed, FC is a function type, not type of props.
UPDATE
You can create higher order function, but I'm not sure if it worth it
const WithGeneric = <T,>(): React.FC<TestProps<T>> =>
(props: TestProps<T>) => (
<span>Some component logic</span>
);
const Test = WithGeneric<Obj>()
The easiest way is to make the generic FC a regular function, not an arrow function. (React.PropsWithChildren<> emulates what React.FC does to your props type.)
function Test<T>(props: React.PropsWithChildren<TestProps<T>>) {
return <span>Some component logic</span>;
}
In my case it was like the following codes:
export interface FormProps<T> {
validator?: AnyObjectSchema;
onSubmit?: (data: T) => void;
}
const Form = <T = any,>({
children,
validator,
}: PropsWithChildren<FormProps<T>>): JSX.Element => {
~~~
And in usage:
type MyType = ...
<Form<MyType>
validation={something}
onSubmit={handleSomething}
>
<SomeCompo />
<AnotherSomeCompo />
</Form>

Generic typing a function that is a prop to a component

What could be the correct way to pass a generic type in a function argument which is one of the prop of the Componenet.
I have tried something like this:
type Props = {
children?: JSX.Element;
onChange?: <T>(a: T, b: number) => void;
}
and using it like (which is giving ts error):
...
handleChange = (a: SomeType[], b: number) => {
return 1;
}
...
<Component onChange={this.handleChange} />
If you just want the function argument a of onChange callback to be one of the values in Props, there is no need to use Generics. onChange does not return anything, Component invokes it with one of the possible prop values.
type Props = {
children?: JSX.Element;
myProp: SomeType[];
onChange?: (
// exclude here the prop value of children and the callback itself
// effectively being a: SomeType[]
a: Props[Exclude<keyof Props, "children" | "onChange">],
b: number
) => void;
};
const Component = (props: Props) => <div>Hello{props.children}</div>;
// declare concrete prop value as example
declare let myProp: SomeType[];
const App = () => {
const handleChange: Props["onChange"] = (a, b) => {
return 1;
};
return (
<Component myProp={myProp} onChange={handleChange} />
);
};
Playground
If Component/Props them selves were generic and receive a type parameter from an outside component (not the case in your example), you could do it like this.
Cheers, hope it helps

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

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.

Resources