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'.
错误原因
问题出在上下文类型与组件泛型的不兼容:
- 你定义的
DataListContext期望接收ContextValue<ItemWithId>类型的值,其中columns是Column<ItemWithId>[]。 - 但
Root组件的泛型TData是ItemWithId的子类型(比如{ id: string; name: string }),生成的columns是Column<TData>[]。 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
相关产品推荐
相关产品推荐

