TypeScript条件泛型实现Select组件onChange强类型优化
/>
// ...
// 使用reduce
<Select
options={[{ label: 'Hello', value: 'hello' }]}
labelKey="label"
valueKey={{ key: 'value', reduce: true }}
onChange={(option) => // option类型为 'hello'
...
/>
# 尝试方案 基于上述背景,我编写了相关代码: ```typescript // 注:此处Element应为JSX.Element,但当前环境不支持 type Options = (string | number | boolean | object | Element)[] type ValueKey<K extends string> = | string | ((...args: any[]) => string) | { key: K; reduce?: boolean }; type ValueOfValueKey< O extends Options, K extends string, VK extends ValueKey<K> > = VK extends { key: K; reduce?: boolean } ? VK['reduce'] extends true ? O[number] extends Record<string, any> ? O[number][VK['key']] : 'not an object' : 'this-branch' : 'this-other-branch'; type Customer = { __typename: 'Customer', id: string; groupId: number; name: string; }; const selectOptions: Customer[] = [ { __typename: 'Customer', id: '12345', groupId: 231231, name: 'CustomerA' } ]; type Test<Key extends string> = ValueOfValueKey<typeof selectOptions, Key, { key: Key, reduce: true }>; type Test2 = Test<'groupId'> // 正确推导为number const fn = <O extends Options, K extends string = ''>(): { onChange: (val: ValueOfValueKey<O, K, ValueKey<K>>) => void } => { return { onChange: (val) => {}}; } // 此处onChange的类型为 (val: "this-other-branch" | "this-branch") => void,不符合预期
示例中Test2能正确推导为number,这符合预期!但在fn的定义中,onChange的类型为(val: "this-other-branch" | "this-branch") => void,而我期望它能推导为具体类型(如number),不清楚问题出在哪里。
使用场景示例
const MyForm = () => { const [selectedCustomer, setSelectedCustomer] = useState<null | string>(null); return ( <form> {/* 不使用reduce */} <Select options={selectOptions} labelKey="label" valueKey={{ key: 'id' }} onChange={(option) => { // 由于option类型为any,开发者需自行排查其类型为Customer,否则可能报错 setSelectedCustomer(option); }} /> {/* 使用reduce */} <Select options={selectOptions} labelKey="label" valueKey={{ key: 'id', reduce: true }} onChange={(option) => { // 由于option类型为any,开发者需自行排查其类型为string(Customer['id']),否则可能报错 setSelectedCustomer(option); }} /> </form> ); };
总体来说,我希望优化类型推导以提升开发体验(DX)。
解决方案
问题出在泛型参数的约束和类型推导逻辑上:
Options类型过于宽泛,object无法让TypeScript识别具体的对象结构,导致类型提取失败。fn中直接使用ValueKey<K>作为ValueOfValueKey的第三个参数,TypeScript无法窄化到具体的{key: K, reduce?: boolean}分支,只能返回条件判断中的固定字面量类型。
以下是修正后的实现:
1. 重新定义核心类型
// 选项类型:支持原始类型或对象 type OptionType = string | number | boolean | Record<string, any> | JSX.Element; // 拆分ValueKey为更明确的子类型,方便条件判断 type ValueKeyString = string; type ValueKeyFn = (...args: any[]) => string; type ValueKeyObject<K extends string = string> = { key: K; reduce?: boolean }; type ValueKey<K extends string = string> = ValueKeyString | ValueKeyFn | ValueKeyObject<K>; // 定义onChange参数的推导逻辑 type OnChangeParam< T extends OptionType, VK extends ValueKey > = VK extends ValueKeyObject<infer K> ? VK['reduce'] extends true ? T extends Record<string, any> ? T[K] : T // 非对象类型直接返回自身 : T // 不启用reduce时返回完整选项对象 : T; // 其他ValueKey类型默认返回完整选项
2. 重构组件泛型定义
把组件的泛型参数细化为选项类型T和valueKey的具体类型VK,让TypeScript能精准推导:
type SelectProps<T extends OptionType, VK extends ValueKey = ValueKey> = { options: readonly T[]; labelKey?: string; valueKey?: VK; onChange?: (val: OnChangeParam<T, VK>) => void; }; // 示例使用 type Customer = { __typename: 'Customer', id: string; groupId: number; name: string; }; const selectOptions: Customer[] = [ { __typename: 'Customer', id: '12345', groupId: 231231, name: 'CustomerA' } ]; // 测试不使用reduce的情况 const selectWithoutReduce: SelectProps<Customer, ValueKeyObject<'id'>> = { options: selectOptions, valueKey: { key: 'id' }, onChange: (option) => { // option类型自动推导为Customer console.log(option.id); } }; // 测试使用reduce的情况 const selectWithReduce: SelectProps<Customer, ValueKeyObject<'id'>> = { options: selectOptions, valueKey: { key: 'id', reduce: true }, onChange: (option) => { // option类型自动推导为string(Customer['id']) console.log(option.length); } };
关键调整点
- 将
Options替换为具体的OptionType,并让组件泛型绑定到单个选项类型T,而非数组类型,这样能更精准地提取对象属性。 - 拆分
ValueKey为子类型,让条件类型能更清晰地匹配到ValueKeyObject分支。 - 在
OnChangeParam中,针对非对象类型的选项直接返回自身,覆盖更多场景。 - 组件Props直接绑定
valueKey的具体类型VK,让TypeScript在使用时能根据传入的valueKey自动推导onChange参数类型。
这样调整后,开发者在使用Select组件时,onChange的参数类型会自动根据valueKey的配置推导出来,无需手动指定类型,大幅提升开发体验。
内容的提问来源于stack exchange,提问作者Kevin Carlos
相关产品推荐
相关产品推荐

