TypeScript中如何通过属性名匹配两个泛型数组中的对象?
我有一个TypeScript泛型抽象类,包含两个泛型数组:
export abstract class DataProvider<V, T extends {token:string}> { inputA: T[] = []; result: V[] = []; }
想要实现一个函数,传入同时存在于T和V中的属性名(需保证类型正确),根据该属性的相同值匹配两个数组中的对象,获取token并返回消息。
目前的实现如下:
public generateMessagesByAttribute<K extends keyof V & keyof T>(attributeName: K): SuccessMessage[] { const successMessages: SuccessMessage[] = []; this.result.forEach(item => { const index = this.inputA.findIndex(input => input[attributeName] === item[attributeName]); // 此处报错 if (index !== -1) { successMessages.push({ taskToken: this.inputA[index].token, output: JSON.stringify(item), }); } }); return successMessages; }
但出现错误:The comparison appears to be unintentional because because the types U[K] and V[K] have no overlaps,我已指定K extends keyof V & keyof T,对此感到困惑。此外,当前实现类型安全性不足,即使属性名同时存在于两个数组,其类型也可能不同,如何解决这一问题?
[更新] 有人建议将inputA的类型扩展为(T&V)[],但这会要求inputA包含T和V的所有属性,不符合需求。我希望让inputA的类型更具体,要求它拥有一个同时存在于T和V中的属性,但不确定当该属性在运行时通过构造函数传入时是否可行。
1. 核心问题分析
报错的本质是:K只是T和V共有的键,但T[K]和V[K]的类型可能完全不兼容(比如一个是number,一个是string),TypeScript无法保证这类值的比较是有意义的,因此抛出警告。
2. 函数层面的类型安全修复
我们需要在泛型约束中额外限定**T[K]和V[K]类型必须兼容**,可以通过双重类型断言或直接在泛型中添加相等约束来实现:
写法一:泛型层面强制类型相等
public generateMessagesByAttribute< K extends keyof V & keyof T, // 断言T[K]和V[K]类型完全一致,否则编译报错 _EnsureSameType extends true = T[K] extends V[K] ? (V[K] extends T[K] ? true : false) : false >(attributeName: K): SuccessMessage[] { const successMessages: SuccessMessage[] = []; this.result.forEach(item => { // 此时类型检查自动通过,因为已确保属性类型一致 const index = this.inputA.findIndex(input => input[attributeName] === item[attributeName]); if (index !== -1) { successMessages.push({ taskToken: this.inputA[index].token, output: JSON.stringify(item), }); } }); return successMessages; }
写法二:返回值约束+临时类型断言
如果觉得泛型断言过于繁琐,可以通过返回值的条件类型约束,配合临时断言绕过检查:
public generateMessagesByAttribute<K extends keyof V & keyof T>( attributeName: K ): T[K] extends V[K] ? SuccessMessage[] : never { const successMessages: SuccessMessage[] = []; this.result.forEach(item => { // 用unknown中转类型,确保比较合法 const index = this.inputA.findIndex(input => input[attributeName] === item[attributeName] as unknown as T[K] ); if (index !== -1) { successMessages.push({ taskToken: this.inputA[index].token, output: JSON.stringify(item), }); } }); return successMessages as T[K] extends V[K] ? SuccessMessage[] : never; }
3. 运行时传入属性名的类型安全方案
如果需要在构造函数中传入匹配属性名,可以在类的泛型参数中提前约束T和V的对应属性类型一致:
export abstract class DataProvider< V, // 约束T必须包含K键,且类型与V[K]一致 T extends {token:string} & Record<K, V[K]>, K extends keyof V & keyof T > { inputA: T[] = []; result: V[] = []; private readonly matchAttribute: K; constructor(matchAttribute: K) { this.matchAttribute = matchAttribute; } public generateMessagesByAttribute(): SuccessMessage[] { const successMessages: SuccessMessage[] = []; this.result.forEach(item => { const index = this.inputA.findIndex(input => input[this.matchAttribute] === item[this.matchAttribute] ); if (index !== -1) { successMessages.push({ taskToken: this.inputA[index].token, output: JSON.stringify(item), }); } }); return successMessages; } }
这种写法在类初始化时就锁定了匹配属性,同时通过泛型约束确保T和V在该属性上的类型一致,彻底避免了类型不兼容问题。
内容的提问来源于stack exchange,提问作者zlZimon

