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

如何为通用表单状态API的setState/getState实现正确类型约束?

实现带TypeScript类型约束的极简表单状态API

我想要创建一个通用的极简表单状态API,并为其中的setState和getState方法添加TypeScript类型约束。例如:

const api: TApi<{ email: string, password: string }> = ......;

// 应触发类型报错:布尔值不符合类型要求
api.setState('fields.email.value', true);

已定义的基础类型

// Utils
type StringKeyOf<T> = Extract<keyof T, string>

// State
type TFieldState<T> = {
  value: T;
}
type TFormState<TValues extends Record<string, any>, TFieldName extends StringKeyOf<TValues>> = {
  submitting: boolean;
  fields: Record<TFieldName, TFieldState<TValues[TFieldName]>>
}

尝试的实现及问题

我的TApi类型实现尝试如下,但存在大量类型错误:

type TApi<TValues extends Record<string, any>, TFieldName extends StringKeyOf<TValues>> = {
  setState<
    TBasePath extends StringKeyOf<TMyFormState>,
    TFieldPath extends `fields.${TFieldName}`,
    TFieldKeyPath extends `fields.${TFieldName}.${StringKeyOf<TFieldState<TValues[TFieldName]>>}`,
    TPath extends TBasePath | TFieldPath | TFieldKeyPath,
    TMyFormState = TFormState<TValues, TFieldName>,
  >(
    path: TPath,
    value: TPath extends TBasePath
            ? TMyState[TPath]
            : TPath extends `fields.${infer UFieldName}`
              ? TMyState['fields'][UFieldName]
              : TPath extends `fields.${infer UFieldName}.${infer UFieldKey}`
                ? TMyState['fields'][UFieldName][UFieldKey]
  ) => void
}

期望TPath支持以下路径:

type TPath = 
// 基础键
| 'submitting'
| 'fields'

// 基础字段
| 'fields.email'
| 'fields.password'

// 字段子键
| 'fields.email.value'
| 'fields.password.value'

修正后的实现方案

核心代码

// 工具类型:提取对象的字符串类型键
type StringKeyOf<T> = Extract<keyof T, string>

// 单个字段状态结构
type TFieldState<T> = {
  value: T;
}

// 表单状态结构:自动推导字段映射
type TFormState<TValues extends Record<string, any>> = {
  submitting: boolean;
  fields: {
    [K in StringKeyOf<TValues>]: TFieldState<TValues[K]>
  }
}

// 生成所有合法的表单路径类型
type FormPath<T extends TFormState<any>> = 
  | StringKeyOf<T>
  | `fields.${StringKeyOf<T['fields']>}`
  | `fields.${StringKeyOf<T['fields']>}.${StringKeyOf<T['fields'][StringKeyOf<T['fields']>]>}`

// 根据路径获取对应的值类型
type PathValue<T extends TFormState<any>, P extends FormPath<T>> =
  P extends StringKeyOf<T> ? T[P] :
  P extends `fields.${infer K}` ? 
    K extends StringKeyOf<T['fields']> ? T['fields'][K] : never :
  P extends `fields.${infer K}.${infer SK}` ?
    K extends StringKeyOf<T['fields']> ?
      SK extends StringKeyOf<T['fields'][K]> ? T['fields'][K][SK] : never : never : never

// 最终的API类型定义
type TApi<TValues extends Record<string, any>> = {
  setState<P extends FormPath<TFormState<TValues>>>(
    path: P,
    value: PathValue<TFormState<TValues>, P>
  ): void;
  getState<P extends FormPath<TFormState<TValues>>>(
    path: P
  ): PathValue<TFormState<TValues>, P>;
}

关键优化点

  1. 简化TFormState:使用映射类型替代手动传递字段名泛型,自动根据TValues生成字段结构
  2. 独立路径生成:通过FormPath统一生成所有合法路径,避免泛型嵌套混乱
  3. 精准类型推导:PathValue通过条件判断,根据不同层级的路径精准匹配对应的值类型
  4. 泛型简化:TApi仅需接收表单值类型TValues,其余类型全部自动推导

验证示例

// 初始化API实例
const api: TApi<{ email: string, password: string }> = {} as TApi<{ email: string, password: string }>;

// 合法调用
api.setState('submitting', true);
api.setState('fields', { email: { value: 'test@example.com' }, password: { value: '123456' } });
api.setState('fields.email', { value: 'new@example.com' });
api.setState('fields.email.value', 'final@example.com');

// 触发类型错误(符合预期)
api.setState('fields.email.value', true); // 布尔值无法赋值给string类型
api.setState('invalid.path', 'test'); // 路径不存在于合法路径列表

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 18:20:34