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

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)。


解决方案

问题出在泛型参数的约束和类型推导逻辑上:

  1. Options类型过于宽泛,object无法让TypeScript识别具体的对象结构,导致类型提取失败。
  2. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 06:01:17