基于顶层与数组键的区分联合类型:Menu组件TS类型定义问题
需求说明
- 实现一个
<Menu>组件,接受items属性,包含两种结构:- 结构1:
Array<{ label: string, onClick: () => void }>——onClick为必填项 - 结构2:
Array<{ label: string, onClick?: () => void }>——onClick为可选项
- 结构1:
- 规则约束:
- 未传入
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> ); }
存在的问题
- 第23行中,TypeScript无法推断当
item.onClick未定义时,props.onFallbackClick一定存在,导致item.onClick || props.onFallbackClick出现类型错误。 - 使用
<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" }} />;
方案说明
- 函数重载:通过两个重载签名明确区分两种props组合,TypeScript会根据传入的props自动匹配对应的类型约束,解决了原代码中类型推断失效的问题。
- 显式分支判断:在组件内部用
if-else分支处理item.onClick和props.onFallbackClick的存在情况,让TypeScript能准确推断每个分支的变量类型,消除原代码中的类型错误。 - 泛型支持:第二个重载中使用泛型
T,确保onFallbackClick的参数类型和items的类型完全一致,保留类型推断的准确性。
内容的提问来源于stack exchange,提问作者Noitidart
相关产品推荐
相关产品推荐

