TypeScript error "not assignable to type 'IntrinsicAttributes'" for React component with extended discriminated union - reactjs

Problem:
I have an array of objects whose interfaces all extend a base interface.
I want to map these objects onto a React component which will route to specialised components for each of the supported child interfaces -- but first, I want to map over the array and extend each object with an onClick handler, whose signature is a generic which I want to specialise to suit whichever child interface it's being mapped onto.
I've come up with a solution that looks as though it should work, but I can't shake this TS error: Type 'AWithClick<T>' is not assignable to type 'IntrinsicAttributes. I see plenty of references in SO and elsewhere to TS errors related to that interface, but none quite seems to apply here.
I checked my solution against this helpful article, and I think the main difference in my implementation is that I'm trying to extend items from the union with specialised onClicks, rather than defining the specialised onClicks in the individual interfaces in the original union. The array of objects comes from a store, and I'm basically mapping its entities to component props, so I want to keep component props interfaces separate from the store interfaces.
Steps to repro:
npx create-react-app repro --template typescript
Replace App.tsx with the following:
import React from 'react';
enum AType { 'as', 'an' }
interface A {
type: AType;
}
interface AS extends A {
type: AType.as;
value: string;
}
interface AN extends A {
type: AType.an;
value: number;
}
type AnyA = AS | AN;
type AWithClick<T extends AnyA> = T & { onClick: (value: T['value']) => void }
const ASComponent = (props: AWithClick<AS>) => <button onClick={() => props.onClick(props.value)}>{props.value}</button>;
const ANComponent = (props: AWithClick<AN>) => <button onClick={() => props.onClick(props.value)}>{props.value}</button>;
const TestComponent = <T extends AnyA>(props: AWithClick<T>) => {
switch (props.type) {
case AType.as: return <ASComponent {...props} />;
case AType.an: return <ANComponent {...props} />;
}
};
const withoutClicks = [
{ type: AType.as, value: 'AS!' } as AS,
{ type: AType.an, value: 1 } as AN,
];
const withClicks = withoutClicks.map(<T extends AnyA>(props: T) => ({
...props,
onClick: (value: T['value']) => { console.log(value) },
}));
const TestComponentMain = () =>
<div>
{withClicks.map(props => <TestComponent {...props} key={props.type} />)}
</div>
export default TestComponentMain;
Now npm start and you'll see the error Type 'AWithClick<T>' is not assignable to type 'IntrinsicAttributes

It seems that typescript can't quite follow the logic to know that you are refining the type adequately.
But TestComponent here does not need to be generic. You can simply declare your argument as the superset of what you support, and then refine the type with conditionals.
This works:
const TestComponent = (props: AWithClick<AnyA>) => {
switch (props.type) {
case AType.as: return <ASComponent {...props} />;
case AType.an: return <ANComponent {...props} />;
}
};
Playground
In general, when troubleshooting error message with generics, it's always good ask the question "does this really need to be generic?"

Related

incompatible function argument in component props

I have a component that takes a list of items, known to have an ID, and a function that filters those items.
The type with an ID is a generic type of item, that all items will have.
But more specific items will include other props.
type GenericItem = {
id: string;
}
type SpecificItem = {
id: string;
someOtherProp: boolean;
}
I also have a function type, that uses the generic type to operate on.
type GenericItemFunction = (item: GenericItem) => boolean;
I then have this component that uses the GenericItem and GenericItemFunction in its props.
type CompProps = {
fn: GenericItemFunction;
items: GenericItem[];
}
const Comp: React.FC<CompProps> = ({ fn, items }) => <></>;
When I try to use this component with the Specific type, I am getting errors saying I cannot use an implementation of GenericItemFunction because the types for item are not compatible.
const App = () => {
const items: SpecificItem[] = [];
const filter = (item: SpecificItem) => item.someOtherProp;
return (
<Comp
fn={filter} // error on `fn` prop
items={items}
/>
)
}
The typescript error I receive is:
Type '(item: SpecificItem) => boolean' is not assignable to type 'GenericItemFunction'.
Types of parameters 'item' and 'item' are incompatible.
Property 'someOtherProp' is missing in type 'GenericItem' but required in type 'SpecificItem'.
I guess I have two questions;
Firstly, Why is there a conflict when both types expect the id: string property?
Secondly, is there a more sane way to do something like this?
My first though was the type for item on the GenericItemFunction could be inferred from the value provided to the items prop in the App component.
But to be completely honest, I'm not sure how that would look...
My other thought was to have the Comp be a generic, but not show to use a react component that uses generics... Seems like jsx/tsx doesn't really support that syntax.
I expect something like this to throw all sorts of errors.
const Comp = <T extends GenericItem,>({ fn, items }) => <></>;
const App = () => {
return <Comp<SpecificType> />;
}
Finally, I did try this and there aren't any errors. But the downside is the type for items is inferred to be any.
type GenericItem = {
id: string;
}
type SpecificItem = {
id: string;
someOtherProp: boolean;
}
type GenericItemFunction <T> = (item: T) => boolean;
type CompProps <T extends GenericItem = any> = {
fn: GenericItemFunction<T>;
items: T[];
}
const Comp: React.FC<CompProps> = ({ fn, items }) => <></>;
const App = () => {
const items: SpecificItem[] = [];
const filter = (item: SpecificItem) => item.someOtherProp;
return (
<Comp
fn={filter}
items={items}
/>
)
}
Here's a link to the playground I've been using.
https://www.typescriptlang.org/play?#code/C4TwDgpgBA4hB2EBOBLAxgSWBAtlAvFAN4BQAkCgCYBcUAzsKvAOYDcJAviSaJFAMqQ0KAGbosuAsRJRZUKrQZM2MuXQD2OCAHlgAC2QAFJOrC0ARuvUAbCAEN47Lj3DQ4iVJmw4AYgFd4NGAUdXgoAB4AFQA+KQAKFG9aSIBKAljLG3tHbhc+AGFNMGNTOgjIqAgAD2x4SjL3ZHFvKQcQWMJSMhF4WkbPCV8AoJD4KOj2OXlvOmSAbQBdJxI0UIYoQpwzKAAleyCAOh988M3ikzA6Dqg4oigegBpp3DKONPxY8OjwgHoJ7lW8HWAEEwGB4u9YqQpoD1okXrRBBBhGIvLhFlJFpM5LDgPcUNZsEh4vCcIihKJmrhIc8cAcNFpdAYkCUwOxVLIkBBgH4kGE4hyphEzoKhXIevgiGJCcguGKxaS6JLFXL5X9BSlOEA
UPDATE:
Why did you use any as default for GenericItem type? Without this I believe it should properly infer Genericitem from GenericItemFunction. – tymzap
Removing the = any for the CompProps typedef causes errors in the Comp declaration...
type CompProps <T extends GenericItem> = {
fn: GenericItemFunction<T>;
items: T[];
}
const Comp: React.FC<CompProps> = ({ fn, items }) => <></>; // this line has the error
Generic type 'CompProps' requires 1 type argument(s).
Meaning, I still have to declare the type somewhere. Meaning I need to know the variation of the GenericItem type before I use the component.
SpecificItem is just a representation of the types that happen to overlap with the GenericItem typedef.
In most cases, the Comp wont know what type will actually be used and any doesn't give any useful information to the author.
I'm hoping for something like...
type CompProps <T extends GenericItem> = {
items: <T extends GenericItem>[];
fn: <infer from T>[];
}
But I'm not sure if this exists, or something like it.
:face_palm:
The CompProps using <T extends GenericType = any> is the correct way to do this.
type CompProps <T extends GenericItem = any> = {
items: T[];
filter: (item: T) => boolean;
}
const Comp: React.FC<CompProps> = (props) => <></>;
The magic is happening here:
const App = () => {
...
const filter = (item: SpecificItem) => item.someOtherProp;
...
}
The const Comp: React.FC<CompProps> doesn't care what the generic type is, hence = any. As long as it has at least the same shape as GenericType.
But inside the App component, where we are defining the method for the filter prop, we are declaring the SpecificItem typedef in the argument. Only that method uses the SpecificItem typedef. CompProps only cares it meets the required shape of GenericItem.
Hope that helps someone.
In most cases, the Comp wont know what type will actually be used and any doesn't give any useful information to the author.
To me, that means Comp is generic. So I would remove the = any on CompProps:
type CompProps<T extends GenericItem> = {
fn: GenericItemFunction<T>;
items: T[];
};
...and then define Comp as generic. There's probably a way to do that with React.FC, but personally I don't use React.FC so I don't know what it is. :-) React.FC doesn't do much of anything for you, and as some people point out, amongst other things it says your component accepts children whether it does or not. I prefer to be explicit about it when my components accept children.
So I'd define Comp like this:
const Comp = <T extends GenericItem>({ fn, items }: CompProps<T>) => {
// Just to show that `fn` and `items` play together:
const example = items.map(fn);
return <>{example}</>;
};
If you wanted it to have children, you can add them easily enough. Use string if you only want to support text, or React.Node if you want to allow just about anything:
// Does allow children:
type Comp2Props<T extends GenericItem> = {
fn: GenericItemFunction<T>;
items: T[];
children: React.ReactNode;
};
const Comp2 = <T extends GenericItem>({ fn, items, children }: Comp2Props<T>) => {
// Just to show that `fn` and `items` play together:
const example = items.map(fn);
return <>{example}</>;
};
Either way, it works well in your scenario:
const App = () => {
const items: SpecificItem[] = [];
const filter = (item: SpecificItem) => item.someOtherProp;
return <>
<Comp
fn={filter}
items={items}
/>
</>;
};
And the types mean it doesn't work in places you probably don't want it to:
const App2 = () => {
const genericItems: GenericItem[] = [];
const filterGeneric = (item: GenericItem) => item.id === "example"; // Silly definition, but...
const specificItems: SpecificItem[] = [];
const filterSpecific = (item: SpecificItem) => item.someOtherProp;
return <>
{/* Desirable error, `fn` can't handle `GenericItem`s, only `SpecificItem`s */}
<Comp
fn={filterSpecific}
items={genericItems}
/>
{/* Works, `fn` handles the supertype of the items, which is fine */}
<Comp
fn={filterGeneric}
items={specificItems}
/>
{/* Desirable error because `Comp` doesn't [now] support `children` */}
<Comp
fn={filterSpecific}
items={specificItems}
>
children here
</Comp>
{/* Works because `Comp2` does support `children`*/}
<Comp2
fn={filterSpecific}
items={specificItems}
>
children here
</Comp2>
</>;
};
Playground link

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.

Typescript - auto inherit props type in React callback

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

Wrapper function for JSX elements, typed via generics

Currently this function works fine
function wrapElement(elem: JSX.Element) {
return ({ ...props }) => React.cloneElement(elem, { ...props })
}
I use it like this, because this way I can get intelliSense for tailwind classes
const Btn = wrapElement(<button className="[A LOT OF TAILWIND UTILITY CLASSES]" />)
But I'm trying to get it to return same type as it receives, so I could get intelliSense for attributes on intrinsic HTML elements.
Right now inferred type is
function wrapElement(elem: JSX.Element): ({ ...props }: {
[x: string]: any;
}) => React.FunctionComponentElement<any>.FunctionComponentElement<any>
I tried some stuff and it all failed with all kinds of errors, at this point it feels like this could be to hacky, but maybe I don't understand something?
It is basically impossible to get the correct props from a JSX.Element. You can achieve the design that you want, but you should pass in the element name and the props as separate arguments rather than passing in a JSX.Element.
This code can accept an element name like 'button' or any React component. It returns a function component with the same props. I am not dropping any props from the returned component because it seems like you are using this for setting defaults rather than dropping requirements.
import React, { ComponentType, ComponentProps } from "react";
const wrapElement = <
C extends keyof JSX.IntrinsicElements | ComponentType<any>
>(
Component: C,
presetProps: Partial<ComponentProps<C>>
) => (props: ComponentProps<C>) => {
const merged: ComponentProps<C> = { ...presetProps, ...props };
return <Component {...merged} />;
};
const Btn = wrapElement("button", {
className: "[A LOT OF TAILWIND UTILITY CLASSES]"
});
const Dbl = wrapElement(Btn, { onClick: () => alert("clicked") });
const Test = () => {
return <Dbl>Click</Dbl>;
};
Typescript Playground Link
You might want to customize your merge behavior to combine className or style properties rather than overriding them.
Note: when I tried to merge the props inline like <Component {...presetProps} {...props} /> I got a weird error "Type Partial<ComponentProps<C>> & ComponentProps<C> is not assignable to type IntrinsicAttributes & LibraryManagedAttributes<C, any>." So that is why I am merging the props on a separate line and annotating the type as ComponentProps<C> instead of the inferred type Partial<ComponentProps<C>> & ComponentProps<C>.

Passing Generics to function component React Typescript

I was just wondering if there is a way of handling this with generics than using any for the following.
I am still trying to wrap my head around the types and generics, just wanted to stay clear of using any. Or would this be an acceptable use case of using any !
SlotRenderer.tsx
// Can this be done ?
interface SlotAProps<T> {
slotData : T[]
}
// Should I stick with
interface SlotAProps {
slotData : any[]
}
const SlotRenderer = (props: SlotAProps) => {
return (
<div>
<p>Renders slot</p>
// for version 1 I could do, currently this throws an error
<p>{props.slotData[0]?.validity}</p>
// for version 2 I could do, currently this throws an error
<p>{props.slotData[0]?.totalPrice}</p>
</div>
)
}
The available types are
interface SampleOne {
id: number;
name: string;
validity: string;
isVisible: boolean;
}
interface SampleTwo {
id: number;
required: true
sampleString: string;
totalPrice: number;
}
The execution would be
// Version 1
const Container = () => {
return (
<ComponentBase
// Passes a component to the Base component
slot={<SlotRenderer<SampleOne> slotData={[{ id: 1, name:'', validity:'', isVisible:'' }]} />}
/>
)
}
// Version 2
const ContainerTwo = () => {
return (
<ComponentBase
// Passes a component to the Base component
slot={<SlotRenderer<SampleTwo> slotData={[{ id: 1, required:true, sampleString:'', totalPrice:10 }]} />}
/>
)
}
The only thing you were missing is adding a type variable to the component function:
const SlotRenderer = <T extends unknown>(props: SlotAProps<T>) => {
// ...
}
So the reason you weren't able to do so before is that you correctly defined the generic props type and know of the syntax for using generic components in tsx, but were not aware that you'd need this extra type variable to forward it to the generic props type.
The extends unknown in the above example is a hack to enable a type parameter in an arrow function in .tsx files, since otherwise the syntax is ambiguous with the opening tag of a component.
You can find more tips for working with typescript+react in the React TypeScript Cheatsheet project.

Resources