对表单值进行 TypeScript 类型推断

解释
简单来说就是,你通过表单描述去定义了表单的属性,然后你现在要做一个表单值对象,这个对象的属性名、值类型都得是描述里面出现过的。
// config
[
{
name: 'link',
type: 'string',
},
{
name: 'age',
type: 'number',
}
]
// 合法的 value
{
link: '',
age: 0,
}
// 不合法的 value
{
link: 666,
age: '',
foo: 'bar'
}
我一直觉得 TS 做不到这种“动态”的检查,但又看到不少表单库有这种功能,也有人问过我怎么实现这种功能,于是我这几天问了一下 LLM 得到了答案,本文就用于解析原理。
代码解析
// 首先定义配置项的类型
type FieldType = 'string' | 'number';
interface FieldConfig {
name: string;
type: FieldType;
}
// 定义类型映射表
type TypeMap = {
'string': string;
'number': number;
}
// 改进类型映射逻辑
type FormValueType<T extends readonly FieldConfig[]> = {
[K in T[number]['name']]: Extract<T[number], { name: K }>['type'] extends keyof TypeMap
? TypeMap[Extract<T[number], { name: K }>['type']]
: never;
}
// 使用示例
const formConfig = [
{
name: 'link',
type: 'string',
},
{
name: 'age',
type: 'number',
}
] as const;
type FormValues = FormValueType<typeof formConfig>;
// 现在这个会正确通过类型检查
const validData: FormValues = {
age: 123,
link: ''
};
FieldType 就是表单项 type 全部的类型名称,光是名称确认不了具体类型,所以 TypeMap 就用来做名称到实际 TS 类型的映射。
FieldConfig 定义了表单项配置的基本类型,name 是项目名称,type 是类型名称。
重点都在 FormValueType 里,他本质上就是描述着一个对象,加上括号可能好看一些:
type FormValueType<T extends readonly FieldConfig[]> = {
[K in T[number]['name']]: (
Extract<T[number], { name: K }>['type'] extends keyof TypeMap
? TypeMap[Extract<T[number], { name: K }>['type']]
: never;
)
}
{
[foo]: bar ? baz : never
}
就是这种形式,所以可以分段来看,T extends readonly FieldConfig[] 是泛型约束,表示 T 一定满足后面的条件,readonly 表示 FieldConfig 数组是只读的,这个一定要加,毕竟要不可变,才能做静态检查时分析出类型。
readonly 同时要结合 as const 来看,如果忽略了 as const,那么表示 formConfig 是可变的,并且 TS 不会为 formConfig 保留具体元素的类型,会直接理解成:
const formConfig: {
name: string;
type: string;
}[]
而加了 as const 之后,就是这样:
const formConfig: readonly [{
readonly name: "link";
readonly type: "string";
}, {
readonly name: "age";
readonly type: "number";
}]
[K in T[number]['name']] 是个“动态”的键,具体的键名是推断出来的,T[number] 表示从数组中获取任意一个元素,['name'] 取元素的 name 属性值作为联合类型,[K in ...] 表示遍历这个类型的所有可能值,后续用 K 取代,在这个例子中 K 就只会是 link 或 age。
看完了键,值实际上是一个三目运算符,先看前面的部分,Extract<T[number], { name: K }>['type'],这里使用 Extract 从元素中提取出满足 name 为 K 的元素,然后取他具体的 type 类型出来。
extends keyof TypeMap 用于判断这个元素 type 的值是否满足 keyof TypeMap 的条件,也就是 type 是否在 TypeMap 的键中,毕竟 type 只能选择我们预先设置的几项,其他的都是不合法的。
如果是,那么值就只能是 TypeMap[Extract<T[number], { name: K }>['type']],这个结合上面的解析看就很简单了,就是将合法的 type 作为键,去取在 TypeMap 中对应的值,也就是我们需要的表单值 TS 类型。
如果不是呢,那么值就是 never,never 就表示不可能发生,前面的判断已经走完了所有的正确的可能,如果走到 never 这一步,那么一定是前面的类型不匹配。
使用时只需要定义 formConfig,然后设置为 as const,再将它的推导类型 typeof formConfig 作为泛型传入 FormValueType,就能得到能经过严格 TS 类型检查的 validData 了。



