Skip to main content

Command Palette

Search for a command to run...

对表单值进行 TypeScript 类型推断

Updated
2 min read
对表单值进行 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 就只会是 linkage

看完了键,值实际上是一个三目运算符,先看前面的部分,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 类型。

如果不是呢,那么值就是 nevernever 就表示不可能发生,前面的判断已经走完了所有的正确的可能,如果走到 never 这一步,那么一定是前面的类型不匹配。

使用时只需要定义 formConfig,然后设置为 as const,再将它的推导类型 typeof formConfig 作为泛型传入 FormValueType,就能得到能经过严格 TS 类型检查的 validData 了。

More from this blog

12 月装机行动记录

(Banner 图文无关) 这几天买了新的装备回来升级配置,除了显卡和散热器,其他都更新了,这两个不更新的原因是太贵了。 由于我不太懂选配置,所以还是让朋友给推荐,我说我的预算在 5000 左右,他就给我转了一个整机,配置大概是这样的: CPU:AMD 9700X 主板:微星 B650M GAMING PLUS WIFI 或者 微星 B650M GAMING WIFI 显卡:木有 内存:英睿达/宇瞻 DDR5 6000 32G 硬盘:1T NVMe PCIe4 SSD 读速 3500M...

Dec 7, 20241 min read
12 月装机行动记录

Homekit + cozylife 插座连接 HA

近期在淘宝上买了个 Homekit + cozylife 的插座,就这种: 一开始只通过 iOS 访问,就是只连接 Homekit,长按开关重置插座,iOS 一扫码就连上了,后来我嫌在外面访问不了,又不想掏钱买苹果的 HomePod,于是就装上了 Home Assistant,打算让设备们都连上 HA,这样就不用交苹果税了。 连接方式还是通过 Homekit,一般来说支持 Homekit 设备都能这样连接,先连上 iOS,然后在 Home App 中移除设备,这时候就能在 HA 中找到设备了:...

Nov 30, 20241 min read
Homekit + cozylife 插座连接 HA

找到了一台祖传的 Ccd 相机

开个玩笑,这台相机其实是我们家在 05 年的时候买的,发票都还在呢,当时花了 4000 块钱!搁现在我都受不了,更别说当年了,看到价格我都震惊了。 相机的型号是索尼的 Cybershot DSC-N1,属于小红书时尚单品 CCD 相机,由于一直放在包装盒里,现在还有 99 新呢。 机子还是正常的,能开机,能拍照,其中一个问题是日期,这款没有 WiFi 功能,所以时间只能保存在本地,不知道是不是 BUG,每次开机都让我重新设置,默认就定在 2005 年 1 月 1 日。 第二个问题是电池,电池应该...

Nov 28, 20241 min read
找到了一台祖传的 Ccd 相机

记录和 ffmpeg 与 LLM 搏斗的两天

要做的 最近在写一个制作视频的功能,就是把 N 个视频合并,然后把对应的 N 张图片,在视频开始的前 5 秒叠加显示出来。 第一口 - diffusion studio 本来我用的是 diffusion studio,这是一个 JS 库,但这玩意性能太差了,因为他要把视频每一帧都读到 canvas 里,数据一多页面就卡住了(为啥要折腾 DOM 呢?) 而且他的 API 十分不好用,作为浏览器脚本你无法读本地数据也就算了,你起码给一个接受纯数据的参数吧,比如 HTML 类型接受源代码,Image ...

Nov 16, 20242 min read
记录和 ffmpeg 与 LLM 搏斗的两天
V

void mian

39 posts