TypeScript泛型适配Record<string|number>出现类型错误的原因
Type '{}' is not assignable to type 'Record<Key, T>' 我原本实现了一个keyBy函数,用来把对象数组转换成以指定数字键为索引的字典:
export function keyBy<T>(items: T[] | undefined, extractId: (item: T) => number): Record<number, T> { const obj: Record<number, T> = {}; if (items) { items.forEach(item => { const id = extractId(item); obj[id] = item; }); } return obj; }
示例用法:
const users = [user1, user2, ...]; const usersById = keyBy(users, u => u.id);
现在我想让这个函数同时支持字符串和数字类型的键,于是修改成了下面的代码:
export function keyBy<Key extends string | number, T>(items: T[] | undefined, extractId: (item: T) => Key): Record<Key, T> { const obj: Record<Key, T> = {}; if (items) { items.forEach(item => { const id = extractId(item); obj[id] = item; }); } return obj; }
结果出现了类型错误:Type '{}' is not assignable to type 'Record<Key, T>'. ts(2322)。我猜测问题出在泛型允许键为string或number的任意类型,但我希望所有键是同一类型,为什么这个写法不生效?
错误原因
你遇到的报错和泛型是否限制键为同一类型无关——你的泛型Key extends string | number已经保证了所有键要么全是string类型,要么全是number类型,不会混合出现。
真正的问题在于Record<Key, T>的类型定义:它要求对象必须包含所有Key类型的属性,但你初始化的{}是空对象,完全不满足这个要求。TypeScript的静态类型检查只会看代码的静态结构,它不知道你后续会通过forEach填充对象属性,所以直接判定空对象和Record<Key, T>不兼容。
解决方案
方案1:类型断言(推荐)
既然你明确知道后续会填充对象内容,可以将初始空对象断言为Record<Key, T>,跳过静态类型检查的这个限制:
export function keyBy<Key extends string | number, T>(items: T[] | undefined, extractId: (item: T) => Key): Record<Key, T> { // 使用类型断言告诉TS这个对象后续会被正确填充 const obj = {} as Record<Key, T>; if (items) { items.forEach(item => { const id = extractId(item); obj[id] = item; }); } return obj; }
方案2:调整返回类型为Partial
如果你想严格遵循类型定义,可以将返回类型改为Partial<Record<Key, T>>,表示对象可能只包含部分Key类型的属性:
export function keyBy<Key extends string | number, T>(items: T[] | undefined, extractId: (item: T) => Key): Partial<Record<Key, T>> { const obj: Partial<Record<Key, T>> = {}; if (items) { items.forEach(item => { const id = extractId(item); obj[id] = item; }); } return obj; }
不过这种写法的副作用是,调用函数后访问属性时需要处理undefined的情况,如果你确定数组中的键是唯一且会被全部填充,方案1更贴合你的需求。
内容的提问来源于stack exchange,提问作者Kevin Renskers

