Creating a Generic React Forward Ref component unassignable to the constraint - reactjs

I'm trying to create a Generic Forward Ref component that accepts a value of generic type and a render function that also needs a value property from that same generic type.
I get the Typescript error: TS2322: Type 'Product' is not assignable to type 'T'.   'Product' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'Draggable'. (L34 # https://github.com/pieterjandebruyne/generic-forward-ref-issue/blob/master/src/GenericForwardRefComponent.tsx)
I already found some posts on stack overflow explaining what and why it could go wrong but still I feel that none of the explanations really apply to this usecase..
I created a public repo to recreate the issue # https://github.com/pieterjandebruyne/generic-forward-ref-issue
Files that are being used are:
App.tsx -> calls the GenericForwardRefComponent
GenericForwardRefComponent.tsx -> should render the ProductRow and pass the generic type
Interfaces.ts -> all types
ProductRow.tsx -> component that should be rendered
The ultimate goal is that I could also have for example a CategoryRow (with value of type Category (that has id: string)) that I could pass to the GenericForwardRefComponent
Is this something that would be possible or am I just trying to stretch the current limits of React/TS? Also I would love to hear an explanation why this error occurs as I don't see the error that could happen with this implementation. This stackoverflow probably addresses this but I can't figure out why/which part triggers it in my code..
How to fix TS2322: "could be instantiated with a different subtype of constraint 'object'"?
I used the generic forward ref approach I found # https://dirask.com/posts/React-forwardRef-with-generic-component-in-TypeScript-D6BoRD

You can see that with this inside the function with this header
const x = <T extends Draggable>(
{
dragOverlay,
renderItem,
value
}: GenericForwardedRefComponentProps<T>,
reference: ForwardedRef<HTMLDivElement>
)
you call this function:
return renderItem({
value,
reference
})
If i simplify, the renderItem has type (value: T) => ... but then in the body, the type of value is always Product.
When the function would be called with T = Product, that would work fine, but consider, that the caller of this function can call the function with T = MyType like this :
interface MyType extends Draggable {
my_prop: string
}
x<MyType>({ renderItem: (x: MyType) => { x.my_prop }, value: { name: "XXX" } })
You can see, that in the function renderItem I'm accessing a field, that is not present in the product type. That's why there is this error. I have fulfilled the contract of the function, but the result is that I'm accessing missing fields.
Anyway, the function is called only once, so you can remove the type parameter and like this:
(
{
dragOverlay,
renderItem,
value
}: GenericForwardedRefComponentProps<Product>,
reference: ForwardedRef<HTMLDivElement>
) => {
Then, typescript will be satisfied.

Related

How can I create a type array from an object that contains the keys?

I think for my scenario there must be a better way to avoid duplicate code, but I can't get it to work..
I have an object like this:
const sectionTypes = {
foo: ...,
bar: ...
} as const;
Based on the object keys, I need to create an array of objects. But only a type array, not a real one.
something like this:
type SectionArray = [
{ type: 'foo', ... },
{ type: 'bar', ... }
]
currently I am creating the array manual, as I have not found a way to create an array dynamically based on the keys. Is there any way to avoid the repetitions? Thanks!
Playground Link with a more detailed recreation: Link
type SectionKeys = typeof sectionTypes
type Section = {type: keyof SectionKeys}
const array: Section[] = [{type: "foo"}]
This way, by annotating the Section[] type, you will be forced by typescript to provide an object with a property type that accepts only the keys of sectionTypes
Providing more props won't cause any typescript error, but there is also no restriction on it. But you got the idea. You can expand your type in whatever way you want.

Change type of state depending on props - React Typescript

I have a form component "AddBooking" that takes the following prop:
type AddBookingProps = { edit?: Booking };
If this "edit" prop is passed in, I want to render the form with the values set. If it isn't passed in, it becomes a "create" blank form. This is how I'm trying to use the state.
const [currentBooking, setBooking] = useState<NewBooking | Booking>(
edit ? edit : emptyBooking
);
emptyBooking is just a load of empty strings in a NewBooking type to initialize the state.
The difference between the types "Booking" and "NewBooking" is that "Booking" has a required "_id" type whereas "NewBooking" doesn't.
The methods I have for editBooking and addBooking required types "Booking" and "NewBooking" respectively. This is where I get type errors:
if (edit) {
bookingContext.editBooking(currentBooking);
handleClose();
} else {
bookingContext.addBooking({
...currentBooking,
bookedBy: uContext.user.username
});
handleClose();
}
I get a type error when calling editBooking:
Argument of type 'NewBooking | Booking' is not assignable to parameter of type 'Booking'.
Property '_id' is missing in type 'NewBooking' but required in type 'Booking'.
Any ideas how I can get around this without the "any" type?
Thanks so much
This maybe a use case for a type guard https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types
A function you define to check that _id is actually on the type. This is probably good practice as you don't want the user to edit a booking that doesn't yet exist

How to narrow down union of generic types in Typescript?

I'm trying to create a form component in React that uses generic types.
The generic type for the form is the initialValues that are passed in.
I can't for the life of me seem to get Typescript to narrow down the type that corresponds to a particular field.
Here's a CodeSandbox with one of my many attempts at trying to get this to work correctly.
I've tried many other things, like adding a 'type' property to each of the fields and then using a switch statement to try and narrow it down that way, but none of them have seemed to work!
Not looking anyone to write this code for me, but a nudge in the right direction would be greatly appreciated.
So as you've probably guessed by now, to do this you can't just define an indexable type like
type FormValues<T> = {
[key: string]: T
}
Because T in this case will be a union that doesn't help too much.
So you can't use indexable type, now the only way I can think stop trying to create the type yourself and just let typescript infer it for you.
Firstly let me change your FormValue declaration a bit, I'll remove ValueOf, while I was playing with your sandbox I forgot what was its purpose, but if I don't think it matters in the way I'm suggesting. So let's type it like this:
type FormValue<TName extends string, TValue> = {
value: TValue
name: TName
pristine: boolean
errors: string[] | null
}
Now this will just take two type arguments, one is the name, another is the value, no unnecessary objects. Now let's type InitialFormValues: it will be just an object with random properties, we won't use it to type anything, only to constraint types:
type InitialValues = {
[key: string]: any
}
// or
type InitialValues = Record<string, any>
Next comes UseFormProps. Here we avoid creating indexable type, that doesn't lead to anything, but we let typescript infer the type for us. We only need to constraint it a bit so that it's not possible to pass p.e. a number as initialValues:
type UseFormProps<T extends InitialValues> = {
initialValues: T
}
Now typescript will automatically choose T and it will be exactly what we need: if initialValues is { foo: 1, bar: 'str' }, it will be { foo: number, bar: string } and nothing more.
Last goes FormValues:
type FormValues<T extends InitialValues> = {
[x in keyof T]: FormValue<x, T[x]>
}
Now FormValues will not be just indexable, but it will contain exactly the properties T contains, also T[x] will not be a union like T[keyof T], it will be different for each value of x so for each property. That's basically it, now it should work as expected. You'll only need to change something in transformInitialValues, because typescript will now complain if you assign empty object to FormValues<whatever>

SharePoint React pnpjs

I am working on a react project for SharePoint framework.
My question is a more or less general question.
I came across a certain problem which I don't understand:
I tried to use sp.search with an object matching the SearchQuery interface, but did this in a class extending react.component
Code:
public search(query: string, properties: Array<string>, rowLimit: number): Promise<SearchResults> {
return new Promise<SearchResults>(async (resolve) => {
let result = await sp.search(<SearchQuery>{
Querytext: query,
SelectProperties: properties,
RowLimit: rowLimit
});
resolve(result);
});
}
As you can see, this is just a basic search function. If I use sp.search in a class that does not extend react.component or if I don't use the searchquery instance, but a plain querystring everything works fine.
I seem to be a type error, but honestly, I don't get what's happening.
Error:
[ts]Argument of type 'Element' is not assignable to parameter of type 'SearchQueryInit'. Type 'Element' is not assignable to type 'ISearchQueryBuilder'.
Property 'query' is missing in type 'Element'. [2345]
[ts] JSX element 'SearchQuery' has no corresponding closing tag. [17008]
[ts] 'SearchQuery' only refers to a type, but is being used as a value here. [2693]
So I decided to just write a service that's using pnpjs and that's totally fine, especially since I didn't plan to use pnpjs in a component, that was just to be a bit faster and have something to test and to work with. but nevertheless, I really would like to understand what is going on. Btw I'm using TypeScript, if that's of interest.
Apparently those errors occur due to a limitation cased by the use of <T> for generic type parameter declarations combined with JSX grammar, refer this issue for a more details (which is marked as a Design Limitation)
To circumvent this error, create the instance of SearchQuery:
const searchQuery: SearchQuery = {
Querytext: query,
SelectProperties: properties,
RowLimit: rowLimit
};
and then pass it into sp.search method:
let result = await sp.search(searchQuery);

Flowtype: generic type with type annotation for dynamic key

I'm using Flow in a React Native project, but I've hit a bump trying to type one of my Props definitions. Specifically, I'm trying to create a component that renders a list that can be filtered with a search input.
My component accepts 3 props:
dataArray: an Array containing objects to be displayed
renderRow: a function that accepts one item from dataArray as only parameter and returns a React Node
filterProp: the name of the attribute to filter on - which should be present on every item in dataArray
To filter this list, I would use something like:
dataArray.filter(item => item[filterProp].includes(searchterm));
As for the Props definition I got this so far:
type Props<T> = {
dataArray: T[],
filterProp: string | number,
renderRow( item: T ): Node,
};
Now I would like Flow to check that every item in dataArray has an attribute (of type string) called <value of filterProp>, but after lots of trying and reading docs, I can't seem to figure out how to do this.
Try $Keys:
type Props<T> = {
dataArray: T[],
filterProp: $Keys<T>,
renderRow( item: T ): Object,
};

Resources