如何更优雅地动态更新keyof T类型的属性值?
TypeScript跨类型字段复制的类型检查问题解决办法
问题描述
尝试将源类型<S>的属性值复制到目标类型<T>的对象中,即便通过代码检查字段存在,TypeScript仍抛出类型索引错误:
● Test suite failed to run src/util/Copy.ts:18:18 - error TS2536: Type 'keyof T' cannot be used to index type 'S'. 18 return source[field] === target[field]; ~~~~~~~~~~~~~ src/util/Copy.ts:33:11 - error TS2536: Type 'keyof S' cannot be used to index type 'T'. 33 match[field] = source[field]; ~~~~~~~~~~~~
需求是让工具函数支持完全不同的类型,不想通过让S继承T来限制仅处理共有字段。报错的原始实现代码如下:
export function copyFieldsWhenMatching<S extends object, T extends object>( sourceArray: S[], targetArray: T[], equalFields: (keyof T)[], fieldsToCopy: (keyof S)[], allowMultipleOccurrences: boolean ): T[] { const toReturn = [...targetArray]; for (const source of sourceArray) { const matches = toReturn.filter((target) => { return equalFields.every((field) => { if (source.hasOwnProperty(field) && target.hasOwnProperty(field)) { return source[field] === target[field]; } return true; }); }); if (!allowMultipleOccurrences && matches.length > 1) { throw new DuplicateMatchesError( "More than one match found for the source object.", matches ); } else if (matches.length >= 1 || allowMultipleOccurrences) { for (const match of matches) { for (const field of fieldsToCopy) { match[field] = source[field]; } } } } return toReturn; }
临时修复方案
目前可以通过类型断言((source as unknown as T)等)绕过检查,但这种方式不够优雅,临时修复代码如下:
import { DuplicateMatchesError } from "../errors"; export function copyFieldsWhenMatching<S extends object, T extends object>( sourceArray: S[], targetArray: T[], equalFields: (keyof T)[], fieldsToCopy: (keyof S)[], allowMultipleOccurrences: boolean, ): T[] { const toReturn = [...targetArray]; for (const source of sourceArray) { const matches = toReturn.filter((target) => { return equalFields.every((field) => { if (source.hasOwnProperty(field) && target.hasOwnProperty(field)) { return ( (source as unknown as T)[field] === target[field] ); } return true; }); }); if (!allowMultipleOccurrences && matches.length > 1) { throw new DuplicateMatchesError( "More than one match found for the source object.", matches, ); } else if (matches.length >= 1 || allowMultipleOccurrences) { for (const match of matches) { for (const field of fieldsToCopy) { (match as unknown as S)[field] = source[field]; } } } } return toReturn; }
更优雅的类型安全实现
方案1:细化泛型约束
通过扩展泛型参数,明确equalFields是S和T的共有键,fieldsToCopy是存在于S且值类型可赋值给T的键,让TypeScript提前识别字段兼容性:
import { DuplicateMatchesError } from "../errors"; export function copyFieldsWhenMatching< S extends object, T extends object, EqualKeys extends keyof S & keyof T, CopyKeys extends keyof S & keyof T >( sourceArray: S[], targetArray: T[], equalFields: EqualKeys[], fieldsToCopy: CopyKeys[], allowMultipleOccurrences: boolean ): T[] { const toReturn = [...targetArray]; for (const source of sourceArray) { const matches = toReturn.filter((target) => { return equalFields.every((field) => source[field] === target[field]); }); if (!allowMultipleOccurrences && matches.length > 1) { throw new DuplicateMatchesError( "More than one match found for the source object.", matches ); } else if (matches.length >= 1 || allowMultipleOccurrences) { for (const match of matches) { for (const field of fieldsToCopy) { match[field] = source[field]; } } } } return toReturn; }
方案2:使用类型守卫细化内部类型
如果需要保留原函数的参数灵活性,可在内部添加类型守卫,让TypeScript在运行时检查的同时识别字段的跨类型兼容性:
import { DuplicateMatchesError } from "../errors"; export function copyFieldsWhenMatching<S extends object, T extends object>( sourceArray: S[], targetArray: T[], equalFields: (keyof T)[], fieldsToCopy: (keyof S)[], allowMultipleOccurrences: boolean ): T[] { const toReturn = [...targetArray]; // 类型守卫:判断字段是否同时属于两个对象 function isSharedKey<K extends string | number | symbol>( key: K, obj1: object, obj2: object ): key is K & keyof typeof obj1 & keyof typeof obj2 { return obj1.hasOwnProperty(key) && obj2.hasOwnProperty(key); } for (const source of sourceArray) { const matches = toReturn.filter((target) => { return equalFields.every((field) => { return isSharedKey(field, source, target) ? source[field] === target[field] : true; }); }); if (!allowMultipleOccurrences && matches.length > 1) { throw new DuplicateMatchesError( "More than one match found for the source object.", matches ); } else if (matches.length >= 1 || allowMultipleOccurrences) { for (const match of matches) { for (const field of fieldsToCopy) { if (isSharedKey(field, source, match)) { match[field] = source[field]; } } } } } return toReturn; }
方案对比
- 方案1通过泛型约束提前限制参数范围,类型检查在函数调用时就生效,更符合TypeScript的类型安全设计理念。
- 方案2保留了原函数的参数灵活性,通过内部类型守卫实现类型细化,避免了粗暴的
unknown断言。
内容的提问来源于stack exchange,提问作者Zied Hamdi
相关产品推荐
相关产品推荐

