Search code examples
reactjstypescriptlistreact-nativetypescript-generics

How to create generic typed component to use React Native's FlatList or SectionList?


Description

I am trying to create a custom ManagedList component which will have props which has type ofFlatListProps<itemT> or SectionListProps<itemT, SectionT = DefaultSectionT> with additional { listType: ListType; dismissKeyboardOnPress?: boolean; dismissElevatedUIElements?: boolean; properties.

Implementation

To do so, I created a custom types ListType and ListProps:

type ListType = 'flat' | 'section';

type ListProps<listT extends ListType, itemT> = (
    listT extends 'flat'
        ? FlatListProps<itemT>
        : SectionListProps<itemT>
    ) & {
        listType: ListType;
        dismissKeyboardOnPress?: boolean;
        dismissElevatedUIElements?: boolean;
    };

Then I created a functional component which accepts generic paramter listT extends ListType as shown below:

function ManagedList<listT extends ListType>(
    props: Readonly<ListProps<listT, any>>
) {
    const {
        listType,
        dismissKeyboardOnPress,
        dismissElevatedUIElements,
        data,
        sections, // destructing sections throws error #1
        renderItem,
        ...otherProps
    } = props;

    ...

    let listCmp;
    if (listType === 'flat') {
        listCmp = (
            <FlatList
                data={data}
                renderItem={renderItem} // defining renderItem prop throws error #2
                onTouchStart={touchStartHandler}
                onLayout={layoutHandler}
                onContentSizeChange={contentSizeChangeHandler}
                onScroll={scrollHandler}
                showsVerticalScrollIndicator={false}
                {...otherProps}
            />
        );
    } else {
        listCmp = (
            <SectionList
                sections={sections}
                renderItem={renderItem} // defining renderItem prop throws error #2
                onTouchStart={touchStartHandler}
                onLayout={layoutHandler}
                onContentSizeChange={contentSizeChangeHandler}
                onScroll={scrollHandler}
                showsVerticalScrollIndicator={false}
                {...otherProps}
            />
        );
    }

    return (
        <View style={styles.listContainer}>
            {listCmp}
            {contentHeight > containerHeight && (
                <Fragment>
                    <View style={styles.scrollbarTrack} />
                    <Animated.View
                        style={[styles.scrollbarThumb, animatedSyles]}
                    />
                </Fragment>
            )}
        </View>
    );

Errors

Error #1

As mentioned in the code, destructing sections from props throws the following error:

Property 'sections' does not exist on type 'Readonly<ListProps<listT, any>>'.ts(2339)

Error #2

2nd error is thrown when defining renderItem props. I believe this is caused because FlatList and SectionList has different renderItem defnitions/types. FlatList has the below type for renderItem prop:

renderItem({
  item: ItemT,
  index: number,
  separators: {
    highlight: () => void;
    unhighlight: () => void;
    updateProps: (select: 'leading' | 'trailing', newProps: any) => void;
  }
}): JSX.Element;

and SectionList's renderItem prop type:

renderItem({
  item: ItemT,
  index: number,
  section: object, // <--- This is different
  separators: {
    highlight: () => void;
    unhighlight: () => void;
    updateProps: (select: 'leading' | 'trailing', newProps: any) => void;
  }
}): JSX.Element;

What I tried?

To solve Error #1, I tried using Type Guards:

const {
    listType,
    dismissKeyboardOnPress,
    dismissElevatedUIElements,
    data,
    renderItem,
    ...otherProps
} = props;
    
let sections;
if(props.sections) {
    sections = props.section;
}

// or

const sections = props?.section ?? undefined;

But all these Type Guard attempts showed a similar error due to sections not being present in props object.

I'm not sure how I can solve Error #2.

NOTE: What I'm trying to accomplish is to have a single custom component to use FlatList or SectionList that have some event handlers assigned to them. Any other better approach to achieve this functionality can be a good answer.


Solution

  • type ListType = 'flat' | 'section';
    type SharedListProps = {
      listType: ListType;
      dismissKeyboardOnPress:boolean;
      dismissElevatedUIElements:boolean;
    };
    type fListProps<T> = FlatListProps<T> & SharedListProps & {listType: 'flat'};
    type sListProps<T> = SectionListProps<T> &
      SharedListProps & {listType: 'section'};
    type ListProps<T> = fListProps<T> | sListProps<T>;
    
    
    function MyList<T>(props: ListProps<T>) {
      const {
        listType,
        dismissKeyboardOnPress,
        dismissElevatedUIElements,
        renderItem,
        /*
        until listType value is checked typescript is unable to infer
        whether flatlist/sectionlist specific properties should exist
         */
        // data,
        // sections,
        ...otherProps
      } = props;
    
     
      if (props.listType == 'flat') {
        // access flatlist specific props here
        console.log(props.data);
        return <FlatList {...props} />;
      }
      // access section list specific props here
      console.log(props.sections);
      return <SectionList {...props} />;
    }