0

I have Tree component, which renders TreeItemsRenderer that draws TreeItem. Each of them are seperate components.
I need to accept dynamic component that replaces default TreeItem, and I need to have all default TreeItem props. (for ex: const CustomComponent extends TreeItem). Tree is used in many places in code. So it needs to be generic.

  • I tried to get all properties from default TreeItem by creating generic type D extends ITreeItemProps<T>.
  • T generic is for item type.
  • All of autocompletion works from outside. Only error is on TreeItemRenderer component with rendering SelectedRowComponent.

here is my components (It may be illogical at some moments, because I deleted unneccesary stuff and simplified logic):

Tree :

import { ITreeItemProps, ITreeItem } from "./types";
import TreeItemRenderer from "./TreeItemRenderer";

export interface ITreeProps<T, D extends ITreeItemProps<T>> {
    items: ITreeItem<T>[];
    getItemKey: (item: T) => string | number; 
    SelectedRowComponent?: React.ComponentType<D>;
    SelectedRowProps?: Partial<React.ComponentProps<ITreeProps<T, D>['SelectedRowComponent'] & {}>>;
    selected?: string | number;
}

export default function Tree<T, D extends ITreeItemProps<T>>(props: ITreeProps<T, D>){
    const {
        items,
        SelectedRowComponent,
        SelectedRowProps,
        getItemKey,
        selected,
    } = props;
    return <>
    <TreeItemRenderer 
        getItemKey={getItemKey}
        items={items}
        SelectedRowComponent={SelectedRowComponent}
        SelectedRowProps={SelectedRowProps}
        selected={selected}
    />
    </>
}

TreeItemRenderer:

import { ITreeItemProps, ITreeItem } from "./types";
import TreeItem from './TreeItem';

export interface ITreeItemRenderer<T, D extends ITreeItemProps<T>>{
    getItemKey: (item: T) => string | number; 
    SelectedRowComponent?: React.ComponentType<D>
    SelectedRowProps?: Partial<React.ComponentProps<ITreeItemRenderer<T, D>['SelectedRowComponent'] & {}>>;
    items: ITreeItem<T>[];
    selected?: string | number;
}

export default function TreeItemRenderer<T, D extends ITreeItemProps<T>>(props: ITreeItemRenderer<T, D>){
    const {
        SelectedRowComponent,
        SelectedRowProps,
        items,
        selected,
        getItemKey,
    } = props;

    return <>
        {items.map((item) => {
            const key = getItemKey(item);
            const isSelected = key === selected;
            return <>
                {
                    isSelected ? 
                    <SelectedRowComponent 
                        {...SelectedRowProps}
                        item={item}
                        getItemKey={getItemKey}
                        isSelected
                    />
                    : <TreeItem 
                        item={item}
                        getItemKey={getItemKey}
                        isSelected
                    />
                }
            </>
        })}
    </>
}

Types component:

export interface ITreeItemProps<T> {
    getItemKey: (item: T) => string | number; 
    isSelected: boolean;
    item: ITreeItem<T>;
}
export type ITreeItem<T> = T & {
    children: ITreeItem<T>[];
};

And my problem is in TreeItemRenderer on line 28

Type '(Partial<D> & { item: ITreeItem<T>; getItemKey: (item: T) => string | number; isSelected: true; }) | (Partial<D & { children?: ReactNode; }> & { ...; })' is not assignable to type 'IntrinsicAttributes & D & { children?: ReactNode; }'.
  Type 'Partial<D> & { item: ITreeItem<T>; getItemKey: (item: T) => string | number; isSelected: true; }' is not assignable to type 'IntrinsicAttributes & D & { children?: ReactNode; }'.
    Type 'Partial<D> & { item: ITreeItem<T>; getItemKey: (item: T) => string | number; isSelected: true; }' is not assignable to type 'D'.
      'Partial<D> & { item: ITreeItem<T>; getItemKey: (item: T) => string | number; isSelected: true; }' is assignable to the constraint of type 'D', but 'D' could be instantiated with a different subtype of constraint 'ITreeItemProps<T>'.ts(2322)
Medet
  • 63
  • 1
  • 10

1 Answers1

0

I'll try to illustrate how your code can break the types. There are literal types in typescript. And D extends ITreeItemProps<T> is assuming you can provide more specialized type of ITreeItemProps<T>:

/*
type ExtendedItemProps = {
    isSelected: false;
    getItemKey: (item: {}) => string | number;
    item: {
        children: ...[];
    };
}
*/
interface ExtendedItemProps extends ITreeItemProps<{}> {
  isSelected: false
}

This type is perfectly extending your ITreeItemProps but here:

<SelectedRowComponent 
    {...SelectedRowProps}
    item={item}
    getItemKey={getItemKey}
    isSelected
/>

your SelectedRowComponent instead of isSeleted: false receiving isSelected: true. And while true is a valid subtype of boolean the type D was instantiaded with different subtype where the type of isSelected field is false. Another valid subtype of type boolean but not the same as true.

Another problem. While your component is expecting properties of type D you're trying to pass it properties SelectedRowProps of type Partial<D> plus { item: ITreeItem<T>; getItemKey: (item: T) => string | number; isSelected: true; }. Imagine we have type D declared as:

interface ExtendedItemProps extends ITreeItemProps<{}> {
  title: string
}

Notice that title is required field. But you can easily supply SelectedRowProps without that field as Partial does exactly that to types.

So here is at least two things that can go wrong with type definitions like that.

There is also a minor issue with overcomplicated type React.ComponentProps<ITreeItemRenderer<T, D>['SelectedRowComponent'] & {}>. That is pretty complicated way to say D.


All in all if your SelectedRowComponent accepts exact types of ITreeItemProps<T> plus some extra props you should write your type without extending ITreeItemProps in generic way. Your ITreeItemRenderer may look like that for example:

export interface ITreeItemRenderer<T, D extends Record<string, any> = {}>{
    getItemKey: (item: T) => string | number; 
    SelectedRowComponent?: React.ComponentType<Omit<D, keyof ITreeItemProps<any>> & ITreeItemProps<T>>
    SelectedRowProps: D & Partial<ITreeItemProps<T>>;
    items: ITreeItem<T>[];
    selected?: string | number;
}

function TreeItemRenderer<T, D>(props: ITreeItemRenderer<T, D>) {...}

playground link

Notice that SelectedRowProps is not optional here. Otherwise you cannot get those extra properties to pass to the SelectedRowComponent component. Here we're essentially telling the typescript that getItemkey, isSelected and item fields has the exact types from ITreeItemProps<T> but component can accept some extra properties from type D and inside SelectedRowProps you should specify only those fields required in D but fields from ITreeItemProps<T> may be omitted.


I hope you understand that even if your SelectedRowProps prop has any of the fields getItemKey, isSelected or item set they all will be overriden by other properties from ITreeItemRenderer here:

<SelectedRowComponent 
    {...SelectedRowProps}
    item={item}
    getItemKey={getItemKey}
    isSelected
/>
aleksxor
  • 7,535
  • 1
  • 22
  • 27