React-Select封装为CustomSelect后TypeScript类型推导异常问题
我封装了CustomSelect组件,基于react-select的Select组件预先填充部分属性,但设置isMulti={false}时,TypeScript无法自动识别onChange的类型应为单值版本:
(newValue: SingleValue<IOption>, actionMeta: ActionMeta<IOption>) => void
而是始终显示为包含多值的联合类型:
(newValue: SingleValue<IOption> | MultiValue<IOption>, actionMeta: ActionMeta<IOption>) => void
相关代码
CustomSelect.tsx
import Select, { components, OptionProps, ValueContainerProps, Props as SelectProps, } from "react-select"; import { IOption } from "../../Table/Types"; import "./CustomSelect.scss"; const { Option: OptionCointainer, ValueContainer } = components; function OptionWithIcon(props: OptionProps<IOption>) { const { label, data: { icon }, } = props; return ( <OptionCointainer {...props} className="selectOption"> {!!icon && <img src={icon} alt={label} />} {label} </OptionCointainer> ); } // react-select 重写值组件时有bug,用children作为临时解决方案 function ValueWithIcon(props: ValueContainerProps<IOption>) { const { getValue, children } = props; const { label, icon } = getValue()[0]; return ( <ValueContainer {...props} className="selectValue"> <> {!!icon && <img src={icon} alt={label} />} {label} <span className="hiddenAbsolute">{children}</span> </> </ValueContainer> ); } function CustomSelect(props: SelectProps<IOption>) { const { options } = props; return ( <Select isDisabled={options ? options.length < 2 : true} components={{ Option: OptionWithIcon, ValueContainer: ValueWithIcon, }} {...props} /> ); } export default CustomSelect;
Filters.tsx
<Select id="currencyFrom" isMulti={false} value={state.fromCurrency} // 鼠标hover时类型为:(newValue: SingleValue<IOption>, actionMeta: ActionMeta<IOption>) => void onChange={onValueChange("fromCurrency")} options={someOptions} isSearchable={false} /> <CustomSelect id="currencyTo" isMulti={false} value={state.toCurrency} // 鼠标hover时类型为:(newValue: SingleValue<IOption> | MultiValue<IOption>, actionMeta: ActionMeta<IOption>) => void onChange={onValueChange("toCurrency")} options={someOptions} isSearchable={false} />
react-select的SelectProps是泛型类型,它的onChange类型会根据第二个泛型参数(表示是否多选)做条件推导。但当前CustomSelect直接接收SelectProps<IOption>,这个写法没有将isMulti的布尔值与onChange的类型做关联约束——TypeScript无法从传入的isMulti={false}反向推断出onChange应该匹配单值版本的类型,只能保留最宽泛的联合类型。
方案一:添加泛型参数绑定多选状态
给CustomSelect添加泛型参数,明确绑定isMulti的类型,让TypeScript能根据传入的isMulti值自动推导onChange的类型:
import Select, { components, OptionProps, ValueContainerProps, Props as SelectProps, } from "react-select"; import { IOption } from "../../Table/Types"; import "./CustomSelect.scss"; const { Option: OptionCointainer, ValueContainer } = components; // 省略OptionWithIcon和ValueWithIcon的代码... // 添加泛型参数IsMulti,默认值为boolean function CustomSelect<IsMulti extends boolean = boolean>( props: SelectProps<IOption, IsMulti> ) { const { options } = props; return ( <Select isDisabled={options ? options.length < 2 : true} components={{ Option: OptionWithIcon, ValueContainer: ValueWithIcon, }} {...props} /> ); } export default CustomSelect;
使用时,传入isMulti={false},TypeScript会自动推断IsMulti为false,从而让onChange的类型变为单值版本。
方案二:用联合类型约束Props关联关系
如果不想使用泛型,可以将CustomSelect的Props拆分为单选、多选两个分支,通过联合类型强制isMulti值与对应Props类型的绑定:
import Select, { components, OptionProps, ValueContainerProps, Props as SelectProps, } from "react-select"; import { IOption } from "../../Table/Types"; import "./CustomSelect.scss"; const { Option: OptionCointainer, ValueContainer } = components; // 省略OptionWithIcon和ValueWithIcon的代码... // 拆分单选和多选的Props类型,强制isMulti与类型关联 type CustomSelectProps = | (SelectProps<IOption, false> & { isMulti: false }) | (SelectProps<IOption, true> & { isMulti: true }); function CustomSelect(props: CustomSelectProps) { const { options } = props; return ( <Select isDisabled={options ? options.length < 2 : true} components={{ Option: OptionWithIcon, ValueContainer: ValueWithIcon, }} {...props} /> ); } export default CustomSelect;
这种方式下,TypeScript会根据传入的isMulti值匹配对应的类型分支,自动推断出正确的onChange类型。
额外优化:修复ValueWithIcon的潜在报错
当前ValueWithIcon直接取getValue()[0],如果单选场景下没有选中值会抛出错误,建议添加空值判断:
function ValueWithIcon(props: ValueContainerProps<IOption>) { const { getValue, children } = props; const selectedValue = getValue()[0]; if (!selectedValue) { return <ValueContainer {...props}>{children}</ValueContainer>; } const { label, icon } = selectedValue; return ( <ValueContainer {...props} className="selectValue"> <> {!!icon && <img src={icon} alt={label} />} {label} <span className="hiddenAbsolute">{children}</span> </> </ValueContainer> ); }
内容的提问来源于stack exchange,提问作者prof chaos

