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

基于顶层与数组键的区分联合类型:Menu组件TS类型定义问题

解决Menu组件的TypeScript类型问题

需求说明

  • 实现一个<Menu>组件,接受items属性,包含两种结构:
    • 结构1:Array<{ label: string, onClick: () => void }> —— onClick为必填项
    • 结构2:Array<{ label: string, onClick?: () => void }> —— onClick为可选项
  • 规则约束:
    • 未传入onFallbackClick时,items必须使用结构1(每个item的onClick必填)
    • 传入onFallbackClick时,items可以使用结构2(每个item的onClick可选)

原尝试代码

import React from 'react';

type TMenuProps<UItem extends TMenuItem> = UItem extends TMenuItemWhenMenuHasFallbackClick ? TMenuPropsWithFallbackClick<UItem> : TMenuPropsWithoutFallbackClick;

type TMenuPropsWithFallbackClick<UItem extends TMenuItemWhenMenuHasFallbackClick> = {
  items: UItem[];
  onFallbackClick: (item: UItem) => void;
};

type TMenuPropsWithoutFallbackClick = {
  items: TMenuItemWhenMenuHasNoFallbackClick[];
  onFallbackClick?: never;
};

type TMenuItemWhenMenuHasFallbackClick = { label: string; onClick: () => void };
type TMenuItemWhenMenuHasNoFallbackClick = { label: string; onClick?: () => void; };
type TMenuItem = TMenuItemWhenMenuHasFallbackClick | TMenuItemWhenMenuHasNoFallbackClick;

function Menu<UItem extends TMenuItem>(props: TMenuProps<UItem>) {
  return (
    <div>
      {props.items.map(item => (
        <button key={item.label} onClick={item.onClick || props.onFallbackClick}>
          {item.label}
        </button>
      ))}
    </div>
  );
}

存在的问题

  1. 第23行中,TypeScript无法推断当item.onClick未定义时,props.onFallbackClick一定存在,导致item.onClick || props.onFallbackClick出现类型错误。
  2. 使用<Menu items={[{label: 'A' as const }]} onFallbackClick={item => console.log('fallback, item.label:', item.label)} />时,onFallbackClick的item参数被推断为any类型,类型推断失效。

解决方案

通过组件函数重载明确区分两种props场景,结合类型守卫处理组件内部的类型判断,代码如下:

import React from 'react';

// 定义两种item类型
type RequiredOnClickItem = { label: string; onClick: () => void };
type OptionalOnClickItem = { label: string; onClick?: () => void };

// 组件props重载签名
function Menu(props: { items: RequiredOnClickItem[]; onFallbackClick?: never }): JSX.Element;
function Menu<T extends OptionalOnClickItem>(props: { items: T[]; onFallbackClick: (item: T) => void }): JSX.Element;

// 组件实现
function Menu(props: {
  items: (RequiredOnClickItem | OptionalOnClickItem)[];
  onFallbackClick?: (item: any) => void;
}) {
  return (
    <div>
      {props.items.map(item => {
        // 显式分支判断,让TypeScript准确推断类型
        const handleClick = () => {
          if (item.onClick) {
            item.onClick();
          } else if (props.onFallbackClick) {
            props.onFallbackClick(item);
          }
        };

        return (
          <button key={item.label} onClick={handleClick}>
            {item.label}
          </button>
        );
      })}
    </div>
  );
}

// 测试用例验证
// 情况1:未传onFallbackClick,items必须包含onClick
<Menu items={[{ label: 'Test', onClick: () => {} }]} />;

// 情况2:传入onFallbackClick,items可省略onClick,且item类型推断正常
<Menu items={[{ label: 'A' as const }]} onFallbackClick={(item) => {
  console.log(item.label); // 正确推断item.label为"A"
}} />;

方案说明

  1. 函数重载:通过两个重载签名明确区分两种props组合,TypeScript会根据传入的props自动匹配对应的类型约束,解决了原代码中类型推断失效的问题。
  2. 显式分支判断:在组件内部用if-else分支处理item.onClick和props.onFallbackClick的存在情况,让TypeScript能准确推断每个分支的变量类型,消除原代码中的类型错误。
  3. 泛型支持:第二个重载中使用泛型T,确保onFallbackClick的参数类型和items的类型完全一致,保留类型推断的准确性。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 16:54:57