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

TypeScript泛型约束类型兼容错误原因及修复方案

TypeScript类型错误原因分析与修复方案

原始代码

import { createContext, useMemo, PropsWithChildren, ReactNode } from 'react';

type ItemWithId = { id: string }

type Column<TData extends ItemWithId> = {
    label: string
    sortable?: boolean
    render: (item: TData, items: TData[]) => ReactNode
}

type ColumnsBuilder<TData extends ItemWithId> = {
    build: (options: Column<TData>) => Column<TData>
}

type ContextValue<TData extends ItemWithId> = {
    columns: Column<TData>[]
    items: TData[]
}

const DataListContext = createContext<ContextValue<ItemWithId> | undefined>(undefined)

type RootProperties<TData extends ItemWithId> = PropsWithChildren<{
    items: TData[]
    getColumns: (builder: ColumnsBuilder<TData>) => Column<TData>[]
}>

export function Root<TData extends ItemWithId>(properties: RootProperties<TData>) {
    const { items, getColumns, children } = properties

    const columns = useMemo(() => {
        return getColumns({
            build(options: Column<TData>) {
                const { sortable = false, ...restOptions } = options

                return {
                    ...restOptions,
                    sortable,
                }
            },
        })
    }, [getColumns])

    const value = useMemo(() => {
        return {
            columns,
            items,
        }
    }, [columns, items])

    return <DataListContext.Provider value={value}>{children}</DataListContext.Provider>
}

错误信息

Type '{ columns: Column<TData>[]; items: TData[]; }' is not assignable to type 'ContextValue<ItemWithId>'.
  Types of property 'columns' are incompatible.
    Type 'Column<TData>' is not assignable to type 'Column<ItemWithId>'.
      Type 'ItemWithId' is not assignable to type 'TData'.
        'ItemWithId' is assignable to the constraint of type 'TData', but 'TData' could be instantiated with a different subtype of constraint 'ItemWithId'.

错误原因

问题出在上下文类型与组件泛型的不兼容:

  1. 你定义的DataListContext期望接收ContextValue<ItemWithId>类型的值,其中columns是Column<ItemWithId>[]。
  2. 但Root组件的泛型TData是ItemWithId的子类型(比如{ id: string; name: string }),生成的columns是Column<TData>[]。
  3. Column<TData>和Column<ItemWithId>是逆变关系:Column<TData>的render函数要求传入更具体的TData类型,而Column<ItemWithId>的render接受更宽泛的ItemWithId类型。TypeScript不允许这种赋值,因为强行赋值后,后续调用render时可能传入不符合TData要求的ItemWithId对象,导致类型不安全。

修复方案

推荐使用第一种安全且简洁的方式:

方式一:添加泛型上下文钩子

保持DataListContext原有定义,新增一个泛型钩子函数来还原正确的TData类型:

import { createContext, useMemo, PropsWithChildren, ReactNode, useContext } from 'react';

type ItemWithId = { id: string }

type Column<TData extends ItemWithId> = {
    label: string
    sortable?: boolean
    render: (item: TData, items: TData[]) => ReactNode
}

type ColumnsBuilder<TData extends ItemWithId> = {
    build: (options: Column<TData>) => Column<TData>
}

type ContextValue<TData extends ItemWithId> = {
    columns: Column<TData>[]
    items: TData[]
}

const DataListContext = createContext<ContextValue<ItemWithId> | undefined>(undefined)

type RootProperties<TData extends ItemWithId> = PropsWithChildren<{
    items: TData[]
    getColumns: (builder: ColumnsBuilder<TData>) => Column<TData>[]
}>

export function Root<TData extends ItemWithId>(properties: RootProperties<TData>) {
    const { items, getColumns, children } = properties

    const columns = useMemo(() => {
        return getColumns({
            build(options: Column<TData>) {
                const { sortable = false, ...restOptions } = options

                return {
                    ...restOptions,
                    sortable,
                }
            },
        })
    }, [getColumns])

    const value = useMemo(() => {
        return {
            columns,
            items,
        }
    }, [columns, items])

    // 类型断言:明确上下文实际值是ContextValue<TData>,且TData继承自ItemWithId,不会出现类型不匹配
    return <DataListContext.Provider value={value as ContextValue<ItemWithId>}>{children}</DataListContext.Provider>
}

// 泛型钩子,让使用者获取对应TData的类型
export function useDataListContext<TData extends ItemWithId>() {
    const context = useContext(DataListContext);
    if (!context) {
        throw new Error('useDataListContext必须在DataList.Root组件内部调用');
    }
    return context as ContextValue<TData>;
}

方式二:泛型上下文(复杂度较高,不推荐)

直接将DataListContext定义为泛型,但需要使用者显式创建上下文实例,灵活性较低:

import { createContext, useMemo, PropsWithChildren, ReactNode } from 'react';

type ItemWithId = { id: string }

type Column<TData extends ItemWithId> = {
    label: string
    sortable?: boolean
    render: (item: TData, items: TData[]) => ReactNode
}

type ColumnsBuilder<TData extends ItemWithId> = {
    build: (options: Column<TData>) => Column<TData>
}

type ContextValue<TData extends ItemWithId> = {
    columns: Column<TData>[]
    items: TData[]
}

// 创建泛型上下文的工厂函数
export function createDataListContext<TData extends ItemWithId>() {
    return createContext<ContextValue<TData> | undefined>(undefined);
}

type RootProperties<TData extends ItemWithId> = PropsWithChildren<{
    items: TData[]
    getColumns: (builder: ColumnsBuilder<TData>) => Column<TData>[]
    context: ReturnType<typeof createDataListContext<TData>>
}>

export function Root<TData extends ItemWithId>(properties: RootProperties<TData>) {
    const { items, getColumns, children, context } = properties

    const columns = useMemo(() => {
        return getColumns({
            build(options: Column<TData>) {
                const { sortable = false, ...restOptions } = options

                return {
                    ...restOptions,
                    sortable,
                }
            },
        })
    }, [getColumns])

    const value = useMemo(() => {
        return {
            columns,
            items,
        }
    }, [columns, items])

    return <context.Provider value={value}>{children}</context.Provider>
}

说明

方式一的核心逻辑是:通过useDataListContext泛型钩子还原使用者需要的TData类型,同时在上下文赋值时的类型断言是安全的——因为Root组件传入的items是TData[],columns的render函数只会收到TData类型的参数,不会出现类型不匹配的情况。

内容的提问来源于stack exchange,提问作者Pavel Perevezencev

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 01:28:10