如何为通用表单状态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>; }
关键优化点
- 简化
TFormState:使用映射类型替代手动传递字段名泛型,自动根据TValues生成字段结构 - 独立路径生成:通过
FormPath统一生成所有合法路径,避免泛型嵌套混乱 - 精准类型推导:
PathValue通过条件判断,根据不同层级的路径精准匹配对应的值类型 - 泛型简化:
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
相关产品推荐
相关产品推荐

