如何创建支持React Native FlatList/SectionList的泛型类型组件?
需求描述
我想要创建一个自定义的ManagedList组件,其props类型为FlatListProps<itemT>或SectionListProps<itemT, SectionT = DefaultSectionT>,并附加{ listType: ListType; dismissKeyboardOnPress?: boolean; dismissElevatedUIElements?: boolean; }这些属性。
当前实现方案
我定义了自定义类型ListType和ListProps:
type ListType = 'flat' | 'section'; type ListProps<listT extends ListType, itemT> = ( listT extends 'flat' ? FlatListProps<itemT> : SectionListProps<itemT> ) & { listType: ListType; dismissKeyboardOnPress?: boolean; dismissElevatedUIElements?: boolean; };
随后创建了接受泛型参数listT extends ListType的函数式组件:
function ManagedList<listT extends ListType>( props: Readonly<ListProps<listT, any>> ) { const { listType, dismissKeyboardOnPress, dismissElevatedUIElements, data, sections, // 解构sections时抛出错误#1 renderItem, ...otherProps } = props; // 省略事件处理函数逻辑... let listCmp; if (listType === 'flat') { listCmp = ( <FlatList data={data} renderItem={renderItem} // 设置renderItem属性时抛出错误#2 onTouchStart={touchStartHandler} onLayout={layoutHandler} onContentSizeChange={contentSizeChangeHandler} onScroll={scrollHandler} showsVerticalScrollIndicator={false} {...otherProps} /> ); } else { listCmp = ( <SectionList sections={sections} renderItem={renderItem} // 设置renderItem属性时抛出错误#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> ); }
遇到的错误
错误#1
解构sections时抛出类型错误:
Property 'sections' does not exist on type 'Readonly<ListProps<listT, any>>'.ts(2339)
错误#2
设置renderItem属性时抛出类型错误,原因是FlatList和SectionList的renderItem类型定义不同:
- FlatList的renderItem类型:
renderItem({ item: ItemT, index: number, separators: { highlight: () => void; unhighlight: () => void; updateProps: (select: 'leading' | 'trailing', newProps: any) => void; } }): JSX.Element;
- SectionList的renderItem类型:
renderItem({ item: ItemT, index: number, section: object, // 此处与FlatList不同 separators: { highlight: () => void; unhighlight: () => void; updateProps: (select: 'leading' | 'trailing', newProps: any) => void; } }): JSX.Element;
已尝试的方案
为解决错误#1,尝试过类型守卫写法,但均因sections不在props类型中而失败:
const { listType, dismissKeyboardOnPress, dismissElevatedUIElements, data, renderItem, ...otherProps } = props; let sections; if(props.sections) { sections = props.section; } // 或 const sections = props?.section ?? undefined;
解决方案
修复错误#1:类型窄化与分支解构
直接解构所有可能的属性会触发类型错误,应该在分支判断中针对不同列表类型单独解构专属属性,或使用类型断言配合类型守卫:
方式1:分支内类型断言
function ManagedList<listT extends ListType>( props: Readonly<ListProps<listT, any>> ) { const { listType, dismissKeyboardOnPress, dismissElevatedUIElements, ...restProps } = props; let listCmp; if (listType === 'flat') { // 窄化类型为FlatListProps const flatProps = restProps as FlatListProps<any>; listCmp = ( <FlatList {...flatProps} onTouchStart={touchStartHandler} onLayout={layoutHandler} onContentSizeChange={contentSizeChangeHandler} onScroll={scrollHandler} showsVerticalScrollIndicator={false} /> ); } else { // 窄化类型为SectionListProps const sectionProps = restProps as SectionListProps<any>; listCmp = ( <SectionList {...sectionProps} onTouchStart={touchStartHandler} onLayout={layoutHandler} onContentSizeChange={contentSizeChangeHandler} onScroll={scrollHandler} showsVerticalScrollIndicator={false} /> ); } // 剩余返回逻辑不变... }
方式2:自定义类型守卫函数
function isFlatListProps<T>(props: any): props is FlatListProps<T> { return props.listType === 'flat'; } function isSectionListProps<T>(props: any): props is SectionListProps<T> { return props.listType === 'section'; } // 在组件中使用 if (isFlatListProps(props)) { // 此时props会被自动窄化为FlatListProps类型 listCmp = <FlatList {...props} {...sharedProps} />; } else { listCmp = <SectionList {...props} {...sharedProps} />; }
修复错误#2:兼容renderItem类型
由于两个列表的renderItem参数结构不同,可通过两种方式解决:
方式1:让ListProps的renderItem支持联合类型
修改ListProps定义,让renderItem接受两种列表的renderItem类型:
type ListRenderItem<ItemT> = | FlatListProps<ItemT>['renderItem'] | SectionListProps<ItemT>['renderItem']; type ListProps<listT extends ListType, itemT> = ( listT extends 'flat' ? Omit<FlatListProps<itemT>, 'renderItem'> & { renderItem?: ListRenderItem<itemT> } : Omit<SectionListProps<itemT>, 'renderItem'> & { renderItem?: ListRenderItem<itemT> } ) & { listType: ListType; dismissKeyboardOnPress?: boolean; dismissElevatedUIElements?: boolean; };
方式2:组件内部适配renderItem
在分支中对renderItem进行包装,统一参数结构:
if (listType === 'flat') { const adaptedRenderItem: FlatListProps<any>['renderItem'] = (info) => { // 调用传入的renderItem,忽略section参数(如果是SectionList类型的renderItem) return renderItem?.(info as any); }; listCmp = ( <FlatList {...flatProps} renderItem={adaptedRenderItem} {...sharedProps} /> ); } else { const adaptedRenderItem: SectionListProps<any>['renderItem'] = (info) => { return renderItem?.(info as any); }; listCmp = ( <SectionList {...sectionProps} renderItem={adaptedRenderItem} {...sharedProps} /> ); }
更简洁的实现思路:使用联合类型props
如果不需要严格区分泛型类型,可直接定义联合类型的props,避免条件类型带来的复杂问题:
type ManagedListProps<ItemT, SectionT = DefaultSectionT> = (FlatListProps<ItemT> & { listType: 'flat' }) | (SectionListProps<ItemT, SectionT> & { listType: 'section' }) & { dismissKeyboardOnPress?: boolean; dismissElevatedUIElements?: boolean; }; function ManagedList<ItemT, SectionT = DefaultSectionT>( props: Readonly<ManagedListProps<ItemT, SectionT>> ) { const { dismissKeyboardOnPress, dismissElevatedUIElements, listType, ...restProps } = props; // 共享事件处理逻辑... if (listType === 'flat') { return ( <View style={styles.listContainer}> <FlatList {...restProps as FlatListProps<ItemT>} onTouchStart={touchStartHandler} // 其他共享属性 /> {/* 滚动条逻辑 */} </View> ); } else { return ( <View style={styles.listContainer}> <SectionList {...restProps as SectionListProps<ItemT, SectionT>} onTouchStart={touchStartHandler} // 其他共享属性 /> {/* 滚动条逻辑 */} </View> ); } }
这种联合类型写法更直观,TypeScript会自动在分支判断中窄化类型,避免条件类型带来的解构错误。
内容的提问来源于stack exchange,提问作者M. Çağlar TUFAN

