Typescript - auto inherit props type in React callback - reactjs

Having this implementation
// CustomComponent.ts
type SaveProps = {
date: string;
name: string;
}
interface IProps {
onSave: (props: SaveProps) => void;
}
const CustomComponent = ({onSave}: IProps) => {
return (
<button onClick={() => onSave({ date: '', name: '' })}>
Click me
</button>
);
};
// ParentComponent.ts
import CustomComponent from './CustomComponent';
export default function ParentComponent () {
const saveData = props => { // what type is props? how to infer this as SaveProps defined in CustomComponent.ts?
}
return (
<CustomComponent onSave={saveData} />
)
}
How can I infer the type of props in saveData as being Props without having to import the type? So that when reading the props in saveData callback, I would automatically know what kind of data CustomComponent is sending back.

In most cases, it is better to export Props type along with the component. In your particular case you can do the following:
import { IProps, CustomComponent } from './CustomComponent';
function ParentComponent () {
const saveData = React.useCallback((props => {
}) as IProps['onSave'], []);
return (
<CustomComponent onSave={saveData} />
)
}
TS Playground

Sorry for one more answer. Comments are restricted by max length.
https://www.typescriptlang.org/docs/handbook/type-inference.html#contextual-typing
TS is smart enough to infer types in different directions, And its managed by a bunch of heuristic algorithms. That's why we can not refactor your example according to some generic ruleset (there is no such).
One more trick without explicit export of Props:
(props => {
}) as Parameters<typeof CustomComponent>[0]['onSave']
TS Playground

Related

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.

Type '({ items }: PropsWithChildren<TodoProps>) => Element[]' is not assignable to type 'FunctionComponent<TodoProps>'

I'm learning Typescript-react and I'm stuck in this error Type '({ items }: PropsWithChildren<TodoProps>) => Element[]' is not assignable to type 'FunctionComponent<TodoProps>' and I am lost on this.
Complete error:
Type '({ items }: PropsWithChildren<TodoProps>) => Element[]' is not assignable to type 'FunctionComponent<TodoProps>'.
Type 'Element[]' is missing the following properties from type 'ReactElement<any, string | ((props: any) => ReactElement<any, string | ... | (new (props: any) => Component<any, any, any>)> | null) | (new (props: any) => Component<any, any, any>)>': type, props, key
Link for code: sandbox repo.
Error happens on the declaration of TodoList function within the TodoList.tsx file.
Any help is appreciated. Cheers!
Code:
import React from "react";
interface Todo {
id: number;
content: string;
completed: boolean;
}
interface TodoProps {
items: Todo[];
}
// v------v here is error
const TodoList: React.FC<TodoProps> = ({ items }) => {
return items.map((item: Todo) => <div key={item.id}>{item.id}</div>);
};
export default TodoList;
Yeah, the error may sound a bit confusing - in essence it says that you can only return a single ReactElement or its equivalent JSX.Element in the function component definition, enforced by React.FC type.
React Fragments solve this limitation, so you can write TodoList in the following manner:
interface TodoProps {
items: Todo[];
}
const TodoList: React.FC<TodoProps> = ({ items }) => (
<React.Fragment>
{items.map((item: Todo) => (
<div key={item.id}>{item.id}</div>
))}
</React.Fragment>
);
Short form:
const TodoList: React.FC<TodoProps> = ({ items }) => (
<>
{items.map(({ id }) => <div key={id}>{id}</div>)}
</>
);
By the way: With pure JS, both class and function components can return multiple elements in an array as render output. Currently, TS has a type incompatibility for returned arrays in function components, so Fragments provide a viable workaround here (in addition to type assertions).
I've encountered a similar error. Eventually I noticed that I'd renamed the file incorrectly from .js to .ts instead of to .tsx when converting a component to a FunctionComponent with TypeScript.
I also got this error when I was trying to return children props from my Loading component like below.
const { loading, children } = props;
return loading ? <p>Loading ... </p> : children;
Then i realize that React is expecting only one return value(1 parent component) from its render method. Therefore I wrapped children props with React.Fragment which is denoted by <></> and that solves my problem. Below is my Loadingcomponent sample, hope that helps someone else.
import { FunctionComponent } from "react";
interface ILoadingProps {
loading: boolean;
}
export const Loading: FunctionComponent<ILoadingProps> = (props) => {
const { loading, children } = props;
return loading ? <p>Loading ... </p> : <>{children}</>;
};
My problem was that I had allowed a "TypeScript Quick Fix" in VSCode to add async to the main functional component interface.
const Welcome: React.FC<TProps> = async (props) => {
After removing it,
const Welcome: React.FC<TProps> = (props) => {
things were back to normal.

React with Typescript -- Generics while using React.forwardRef

I am trying to create a generic component where a user can pass the a custom OptionType to the component to get type checking all the way through. This component also required a React.forwardRef.
I can get it to work without a forwardRef. Any ideas? Code below:
WithoutForwardRef.tsx
export interface Option<OptionValueType = unknown> {
value: OptionValueType;
label: string;
}
interface WithoutForwardRefProps<OptionType> {
onChange: (option: OptionType) => void;
options: OptionType[];
}
export const WithoutForwardRef = <OptionType extends Option>(
props: WithoutForwardRefProps<OptionType>,
) => {
const { options, onChange } = props;
return (
<div>
{options.map((opt) => {
return (
<div
onClick={() => {
onChange(opt);
}}
>
{opt.label}
</div>
);
})}
</div>
);
};
WithForwardRef.tsx
import { Option } from './WithoutForwardRef';
interface WithForwardRefProps<OptionType> {
onChange: (option: OptionType) => void;
options: OptionType[];
}
export const WithForwardRef = React.forwardRef(
<OptionType extends Option>(
props: WithForwardRefProps<OptionType>,
ref?: React.Ref<HTMLDivElement>,
) => {
const { options, onChange } = props;
return (
<div>
{options.map((opt) => {
return (
<div
onClick={() => {
onChange(opt);
}}
>
{opt.label}
</div>
);
})}
</div>
);
},
);
App.tsx
import { WithoutForwardRef, Option } from './WithoutForwardRef';
import { WithForwardRef } from './WithForwardRef';
interface CustomOption extends Option<number> {
action: (value: number) => void;
}
const App: React.FC = () => {
return (
<div>
<h3>Without Forward Ref</h3>
<h4>Basic</h4>
<WithoutForwardRef
options={[{ value: 'test', label: 'Test' }, { value: 1, label: 'Test Two' }]}
onChange={(option) => {
// Does type inference on the type of value in the options
console.log('BASIC', option);
}}
/>
<h4>Custom</h4>
<WithoutForwardRef<CustomOption>
options={[
{
value: 1,
label: 'Test',
action: (value) => {
console.log('ACTION', value);
},
},
]}
onChange={(option) => {
// Intellisense works here
option.action(option.value);
}}
/>
<h3>With Forward Ref</h3>
<h4>Basic</h4>
<WithForwardRef
options={[{ value: 'test', label: 'Test' }, { value: 1, label: 'Test Two' }]}
onChange={(option) => {
// Does type inference on the type of value in the options
console.log('BASIC', option);
}}
/>
<h4>Custom (WitForwardRef is not generic here)</h4>
<WithForwardRef<CustomOption>
options={[
{
value: 1,
label: 'Test',
action: (value) => {
console.log('ACTION', value);
},
},
]}
onChange={(option) => {
// Intellisense SHOULD works here
option.action(option.value);
}}
/>
</div>
);
};
In the App.tsx, it says the WithForwardRef component is not generic. Is there a way to achieve this?
Example repo: https://github.com/jgodi/generics-with-forward-ref
Thanks!
Creating a generic component as output of React.forwardRef is not directly possible 1 (see bottom). There are some alternatives though - let's simplify your example a bit for illustration:
type Option<O = unknown> = { value: O; label: string; }
type Props<T extends Option<unknown>> = { options: T[] }
const options = [
{ value: 1, label: "la1", flag: true },
{ value: 2, label: "la2", flag: false }
]
Choose variants (1) or (2) for simplicity. (3) will replace forwardRef by usual props. With (4) you globally chance forwardRef type definitions once in the app.
Playground variants 1, 2, 3
Playground variant 4
1. Use type assertion ("cast")
// Given render function (input) for React.forwardRef
const FRefInputComp = <T extends Option>(p: Props<T>, ref: Ref<HTMLDivElement>) =>
<div ref={ref}> {p.options.map(o => <p>{o.label}</p>)} </div>
// Cast the output
const FRefOutputComp1 = React.forwardRef(FRefInputComp) as
<T extends Option>(p: Props<T> & { ref?: Ref<HTMLDivElement> }) => ReactElement
const Usage11 = () => <FRefOutputComp1 options={options} ref={myRef} />
// options has type { value: number; label: string; flag: boolean; }[]
// , so we have made FRefOutputComp generic!
This works, as the return type of forwardRef in principle is a plain function. We just need a generic function type shape. You might add an extra type to make the assertion simpler:
type ForwardRefFn<R> = <P={}>(p: P & React.RefAttributes<R>) => ReactElement |null
// `RefAttributes` is built-in type with ref and key props defined
const Comp12 = React.forwardRef(FRefInputComp) as ForwardRefFn<HTMLDivElement>
const Usage12 = () => <Comp12 options={options} ref={myRef} />
2. Wrap forwarded component
const FRefOutputComp2 = React.forwardRef(FRefInputComp)
// ↳ T is instantiated with base constraint `Option<unknown>` from FRefInputComp
export const Wrapper = <T extends Option>({myRef, ...rest}: Props<T> &
{myRef: React.Ref<HTMLDivElement>}) => <FRefOutputComp2 {...rest} ref={myRef} />
const Usage2 = () => <Wrapper options={options} myRef={myRef} />
3. Omit forwardRef alltogether
Use a custom ref prop instead. This one is my favorite - simplest alternative, a legitimate way in React and doesn't need forwardRef.
const Comp3 = <T extends Option>(props: Props<T> & {myRef: Ref<HTMLDivElement>})
=> <div ref={myRef}> {props.options.map(o => <p>{o.label}</p>)} </div>
const Usage3 = () => <Comp3 options={options} myRef={myRef} />
4. Use global type augmentation
Add following code once in your app, perferrably in a separate module react-augment.d.ts:
import React from "react"
declare module "react" {
function forwardRef<T, P = {}>(
render: (props: P, ref: ForwardedRef<T>) => ReactElement | null
): (props: P & RefAttributes<T>) => ReactElement | null
}
This will augment React module type declarations, overriding forwardRef with a new function overload type signature. Tradeoff: component properties like displayName now need a type assertion.
1 Why does the original case not work?
React.forwardRef has following type:
function forwardRef<T, P = {}>(render: ForwardRefRenderFunction<T, P>):
ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>>;
So this function takes a generic component-like render function ForwardRefRenderFunction, and returns the final component with type ForwardRefExoticComponent. These two are just function type declarations with additional properties displayName, defaultProps etc.
Now, there is a TypeScript 3.4 feature called higher order function type inference akin to Higher-Rank Types. It basically allows you to propagate free type parameters (generics from the input function) on to the outer, calling function - React.forwardRef here -, so the resulting function component is still generic.
But this feature can only work with plain function types, as Anders Hejlsberg explains in [1], [2]:
We only make higher order function type inferences when the source and target types are both pure function types, i.e. types with a single call signature and no other members.
Above solutions will make React.forwardRef work with generics again.
I discovered this question from reading this blog post, and I think there is a more straight-forward way of accomplishing this than the current accepted answer has proposed:
First we define an interface to hold the type of the component using something called a call signature in typescript:
interface WithForwardRefType extends React.FC<WithForwardRefProps<Option>> {
<T extends Option>(props: WithForwardRefProps<T>): ReturnType<React.FC<WithForwardRefProps<T>>>
}
Notice how the function signature itself is declared as generic, not the interface - this is the key to making this work. The interface also extends React.FC in order to expose some useful Component properties such as displayName, defaultProps, etc.
Next we just supply that interface as the type of our component, and without having to specify the type of the props, we can pass that component to forwardRef, and the rest is history...
export const WithForwardRef: WithForwardRefType = forwardRef((
props,
ref?: React.Ref<HTMLDivElement>,
) => {
const { options, onChange } = props;
return (
<div ref={ref}>
{options.map((opt) => {
return (
<div
onClick={() => {
onChange(opt);
}}
>
{opt.label}
</div>
);
})}
</div>
);
});
Sandbox link here
References:
https://stackoverflow.com/a/73795451/2089675

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