You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何创建支持React Native FlatList/SectionList的泛型类型组件?

自定义ManagedList组件的类型问题解决

需求描述

我想要创建一个自定义的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 19:32:01