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

TypeScript混合已知与未知属性时调用set函数报错的解决咨询

问题:React Context中TypeScript混合已知属性与未知属性的类型错误

尝试在React Context中同时包含动态索引属性(存储各类资源)和已知方法(添加资源),代码大致如下:

interface IClient { id: string; /* 其他属性 */ }
interface ICompany { id: string; /* 其他属性 */ }
interface IUser { id: string; /* 其他属性 */ }

interface IFoo {
  [key: string]: IClient | ICompany | IUser;
}

interface IBar {
  set: (arg0: IClient | ICompany | IUser) => void;
}

type FooBar = IFoo | IBar;

const ItemContext = createContext<FooBar>(null);

export function useItems() {
  return useContext<FooBar>(ItemContext);
}

export function ItemProvider({ children }) {
  const [items, setItems] = useState<FooBar>(null);

  function addItem(resource: IClient | ICompany | IUser) {
    setItems({
      ...items,
      ...{ [resource.id]: resource },
    });
  }

  return (
    <ItemContext.Provider
      value={{
        ...items,
        set: addItem,
      }}
    >{children}</ItemContext.Provider>
  );
}

消费上下文调用set方法时出现错误:

export function useRouter() {
  const items = useItems();

  function test(clientObject: IClient) {
    items.set(clientObject); // 此处抛出错误
  }
}

错误信息:

This expression is not callable.
Not all constituents of type 'IClient | ICompany | IUser | ((arg0: IClient | ICompany | IUser) => void)' are callable.
Type 'IClient' has no call signatures.ts(2349)

请问TypeScript中是否有可行方案实现已知与未知属性的混合?


解决方案

问题根源

原代码中FooBar使用了联合类型(IFoo | IBar),表示值要么是仅含动态索引的对象,要么是仅含set方法的对象。但实际上下文的value同时包含两者,TypeScript无法确定items.set到底是IBar中的函数,还是IFoo中某个索引对应的资源(如IClient),因此抛出类型错误。

正确实现:使用交叉类型

将联合类型改为交叉类型(IFoo & IBar),表示值同时拥有IFoo的动态索引属性和IBar的set方法。同时修正初始值的类型问题:

interface IClient { id: string; /* 其他属性 */ }
interface ICompany { id: string; /* 其他属性 */ }
interface IUser { id: string; /* 其他属性 */ }

interface IFoo {
  [key: string]: IClient | ICompany | IUser;
}

interface IBar {
  set: (arg0: IClient | ICompany | IUser) => void;
}

// 改为交叉类型:同时拥有动态索引和set方法
type FooBar = IFoo & IBar;

// 初始化空对象作为上下文默认值,通过类型断言处理临时类型兼容
const ItemContext = createContext<FooBar>({} as FooBar);

export function useItems() {
  return useContext(ItemContext);
}

export function ItemProvider({ children }) {
  // useState仅存储资源部分,排除set方法
  const [items, setItems] = useState<Omit<FooBar, 'set'>>({});

  function addItem(resource: IClient | ICompany | IUser) {
    // 使用函数式更新避免闭包依赖问题
    setItems(prev => ({
      ...prev,
      [resource.id]: resource,
    }));
  }

  return (
    <ItemContext.Provider
      value={{
        ...items,
        set: addItem,
      }}
    >{children}</ItemContext.Provider>
  );
}

关键修改说明

  1. 交叉类型替代联合类型:IFoo & IBar明确表示对象同时具备动态索引属性和set方法,TypeScript可以准确推断set是可调用的函数。
  2. 修正初始值类型:null不符合交叉类型的要求,改用空对象配合类型断言处理默认值的临时类型问题,同时拆分状态为仅存储资源的部分,避免类型混淆。
  3. 优化状态更新逻辑:使用函数式更新(prev => ...)避免依赖闭包中的旧items值,确保状态更新的准确性。

修改后,消费上下文时可以正常调用items.set,不会再出现类型错误。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 15:35:39