TypeScript:如何强制约束继承Record的泛型类型保持一致性
解决TypeScript泛型FormErrors的类型约束问题
问题背景
我编写了一个用于处理验证逻辑的泛型类型FormErrors,用来生成对应数据类型的错误结构:
定义基础数据类型:
type User = { name: string; cars: Car[] } type Car = { name: string }
错误结构规则:
- 若属性存在错误,用字符串数组定义错误信息,错误对象仅包含有错误的属性,无错误的属性不会出现:
errors = { name: ["is required"] } - 当错误存在于嵌套数组对象中时,生成对应关联对象的错误数组,内部属性遵循相同结构:
errors = { cars: [{name: ["is required"]}] }
现有代码
当前的FormErrors类型定义如下:
type FormErrors<T extends Record<string, unknown>> = { [K in keyof Partial<T>]: T[K] extends Record<string, unknown>[] ? Array<FormErrors<T[K][number]>> : Array<string>; };
使用示例:
let errors: FormErrors<User>; errors.name // string[] | undefined errors.cars // FormErrors<Car>[] | undefined
问题描述
当前TypeScript并未强制约束该泛型的类型参数。例如,一个声明接收FormErrors<User>的方法,会允许传入FormErrors<Car>而无任何类型警告。这是因为二者的可选属性集合存在重叠(都包含name属性),TypeScript的结构类型系统会认为它们兼容。
解决方案
要解决这个问题,我们可以给FormErrors添加一个唯一的类型标记属性,让不同类型参数的FormErrors在结构上产生差异,从而被TypeScript视为不同的类型。
方案1:使用唯一符号标记
// 声明一个唯一符号,用于标记FormErrors的类型参数 declare const formErrorsTag: unique symbol; type FormErrors<T extends Record<string, unknown>> = { // 保留原有的错误属性映射,将Partial<T>改为keyof T的可选属性(语义更准确) [K in keyof T]?: T[K] extends Record<string, unknown>[] ? Array<FormErrors<T[K][number]>> : Array<string>; } & { // 添加标记属性,其类型为原类型T,确保不同T的FormErrors不兼容 [formErrorsTag]: T; };
方案2:使用字符串字面量标记(简化版)
如果不需要严格的类型参数关联,也可以使用带有类型参数标识的字符串字面量:
type FormErrors<T extends Record<string, unknown>> = { [K in keyof T]?: T[K] extends Record<string, unknown>[] ? Array<FormErrors<T[K][number]>> : Array<string>; } & { // 添加只读的字符串标记,区分不同类型参数的FormErrors readonly [Symbol.toStringTag]: `FormErrors<${string}>`; };
效果验证
修改后,当尝试将FormErrors<Car>传入预期FormErrors<User>的方法时,TypeScript会抛出类型不兼容的错误,实现了我们需要的强制约束。
内容的提问来源于stack exchange,提问作者James St-Pierre
相关产品推荐
相关产品推荐

