动态表单(配置化表单)是中后台的常见需求:用一份 Schema 描述字段类型、标签、校验规则,由引擎渲染并校验。难点在于"Schema 与表单值类型联动"——字段是 number 类型时表单值就该是 number,是 checkbox 时就该是 boolean。本 demo 用映射类型 + 条件类型从 Schema 自动推导表单值类型,并配合 as const 保留字面量,让 Schema 既校验合法又能驱动类型推导。
// 表单 Schema 类型:字段定义、校验规则与类型推导
// 驱动动态表单生成,从 Schema 推导出表单值类型
// 1. 字段类型枚举
type FieldType = 'text' | 'number' | 'select' | 'checkbox';
// 2. 校验规则
interface ValidationRule {
required?: boolean;
min?: number;
max?: number;
pattern?: string;
message?: string;
}
// 3. 字段定义(数组字段用 readonly 以兼容 as const)
interface FieldDef {
name: string;
label: string;
type: FieldType;
rules?: readonly ValidationRule[];
options?: readonly { label: string; value: string }[];
}
// 4. 表单 Schema:字段集合
type FormSchema = Record<string, FieldDef>;
// 5. 从 Schema 推导表单值类型(按 type 字段映射)
type FormValues<S> = {
[K in keyof S]: S[K] extends { type: 'number' }
? number
: S[K] extends { type: 'checkbox' }
? boolean
: string;
};
// 应用:定义登录表单 Schema(as const 保留字面量类型)
const loginSchema = {
username: { name: 'username', label: '用户名', type: 'text', rules: [{ required: true }] },
password: { name: 'password', label: '密码', type: 'text', rules: [{ required: true, min: 6 }] },
remember: { name: 'remember', label: '记住我', type: 'checkbox' },
age: { name: 'age', label: '年龄', type: 'number', rules: [{ min: 0, max: 150 }] },
} as const satisfies FormSchema;
// 自动推导表单值类型
type LoginValues = FormValues<typeof loginSchema>;
// => { username: string; password: string; remember: boolean; age: number }
// 校验函数:接收符合 Schema 的值
function validate<S extends FormSchema>(schema: S, values: FormValues<S>): boolean {
// 遍历字段执行校验规则...
return true;
}
const ok = validate(loginSchema, { username: 'wjs', password: '123456', remember: true, age: 18 });
// => ok: boolean
FormValues<S>:遍历 Schema 的每个字段键,根据 type 映射出 number/boolean/string,让表单值类型与 Schema 自动联动。S[K] extends { type: 'number' }:用 extends 匹配字段的 type 字面量,分支决定该字段的值类型,是"按字段元信息推导类型"的核心。as const 保留字面量:不加 as const,type: 'text' 会被推断为 string,推导就失效;as const 把 'text'/'number'/'checkbox' 锁定为字面量,条件类型才能精确匹配。satisfies FormSchema:校验 Schema 合法性(字段结构是否符合 FieldDef)但不拓宽类型,既保证约束又保留字面量信息,是 as const 的最佳搭档。readonly ValidationRule[] 才能通过 satisfies 校验。// 表单值类型随 Schema 自动变化
type V = FormValues<typeof loginSchema>;
// => { username: string; password: string; remember: boolean; age: number }
// 值类型错误会被编译期拦截
// validate(loginSchema, { username: 'wjs', password: '123', remember: 'yes', age: 18 });
// 报错:remember 应为 boolean
// 不同 Schema 推导不同值类型
const settingSchema = {
theme: { name: 'theme', label: '主题', type: 'select', options: [{ label: '深色', value: 'dark' }] },
volume: { name: 'volume', label: '音量', type: 'number' },
} as const satisfies FormSchema;
type SettingValues = FormValues<typeof settingSchema>;
// => { theme: string; volume: number }(select => string)
// 实战:渲染引擎根据 Schema 生成对应输入控件
function renderField(field: FieldDef) {
switch (field.type) {
case 'number': return '<input type=number>';
case 'checkbox': return '<input type=checkbox>';
default: return '<input type=text>';
}
}