TypeScript 类型体操:一个实战案例
前阵子重构一个表单组件库,遇到个类型定义的问题。说实话一开始觉得用 any 算了,但想到以后维护的人会骂我,还是硬着头皮搞了。
场景
我们有个动态表单配置,大概长这样:
type FieldType = 'text' | 'number' | 'select' | 'date';
interface BaseField {
name: string;
label: string;
required?: boolean;
}
interface TextField extends BaseField {
type: 'text';
maxLength?: number;
}
interface NumberField extends BaseField {
type: 'number';
min?: number;
max?: number;
}
interface SelectField extends BaseField {
type: 'select';
options: { label: string; value: string }[];
}
问题来了,怎么定义一个 Field 类型,让它根据 type 字段自动推断出具体的字段类型?
尝试 1:联合类型
type Field = TextField | NumberField | SelectField;
function getOptions(field: Field) {
if (field.type === 'select') {
return field.options; // OK,TypeScript 能推断
}
return undefined;
}
这个能用,但是有个问题——当我们定义表单配置数组的时候:
const fields: Field[] = [
{ type: 'text', name: 'username', label: '用户名', maxLength: 20 },
{ type: 'select', name: 'role', label: '角色' } // 报错!缺少 options
];
类型太宽泛了,TypeScript 不知道 type: 'select' 必须有 options。
尝试 2:泛型 + 映射
折腾了一会儿,想到用映射类型:
type FieldMap = {
text: TextField;
number: NumberField;
select: SelectField;
date: DateField;
};
type Field<T extends FieldType = FieldType> = T extends FieldType
? FieldMap[T]
: never;
还是不太对,这样 Field 还是联合类型。
最终方案
type FieldConfig<T extends FieldType[]> = {
[K in T[number]]: FieldMap[K];
}[T[number]];
// 使用
const fields = [
{ type: 'text', name: 'username', label: '用户名' },
{ type: 'select', name: 'role', label: '角色', options: [] },
] as const satisfies FieldConfig<('text' | 'select')[]>;
好吧,这个也有点绕。实际上最后项目里用的是另一种方案——函数生成器:
function defineField<T extends FieldType>(
field: FieldMap[T]
): FieldMap[T] {
return field;
}
// 这样写就有完整类型提示了
const fields = [
defineField({ type: 'text', name: 'username', label: '用户名' }),
defineField({ type: 'select', name: 'role', label: '角色', options: [] }),
];
简单粗暴,但有效。
后记
类型体操确实有用,但也别过度。能用简单方案解决的,就别为了炫技搞一堆 infer 和条件类型。代码是给人看的,能让 IDE 提示正确就够了。
另外推荐个网站叫 type-challenges,有空刷刷还挺有意思的,虽然工作中用不到那么复杂的。