如何基于Entity与Collection接口实现实体指定路径值自增的辅助函数?
实现类型安全的Collection实体指定路径自增辅助函数
首先先把你给出的接口定义整理好,同时补充EntityId的基础类型定义(原接口里用到了但未明确):
/** 实体接口 */ export interface Entity extends Object { readonly id: EntityId; } /** 基于集合的状态接口 */ export interface Collection<T extends Entity> extends Object { readonly entities: { [key: string]: T }; readonly ids: EntityId[]; } // 定义EntityId为字符串或数字类型,适配常见的ID场景 export type EntityId = string | number;
接下来,我们要实现的辅助函数需要满足两个核心要求:
- 能沿着指定路径找到目标实体的数值字段并自增
- 严格的类型安全:只允许传入指向
number类型字段的路径,且路径必须属于实体的子属性
第一步:定义类型工具实现路径校验
首先需要写一个递归类型,用来自动提取实体中所有值为number类型的属性路径,这样TypeScript就能在编译期帮我们校验路径的合法性:
// 递归提取实体中所有值为number类型的属性路径 type DeepNumberPath<T> = T extends number ? [] : { [K in keyof T]: K extends string ? [...DeepNumberPath<T[K]>, K] extends infer P ? P extends any[] ? P[number] extends never ? never : P : never : never : never }[keyof T]; // 辅助类型:反转路径数组,方便从实体根节点开始遍历到目标字段 type ReversePath<T> = T extends [...infer Rest, infer Last] ? [Last, ...ReversePath<Rest>] : [];
第二步:实现核心的increment函数
因为你的接口里都是readonly属性,所以我们遵循不可变原则——不修改原集合,而是返回一个新的更新后的集合:
export function increment<T extends Entity>( collection: Collection<T>, entityId: EntityId, path: DeepNumberPath<T> ): Collection<T> { const entityKey = String(entityId); const targetEntity = collection.entities[entityKey]; // 如果指定ID的实体不存在,直接返回原集合 if (!targetEntity) { return collection; } // 递归创建更新后的实体对象(保证不可变性) const updateNestedField = (obj: any, pathSegments: any[]): any => { if (pathSegments.length === 1) { return { ...obj, [pathSegments[0]]: obj[pathSegments[0]] + 1 }; } const [currentSegment, ...remainingSegments] = pathSegments; return { ...obj, [currentSegment]: updateNestedField(obj[currentSegment], remainingSegments) }; }; // 反转路径,从根节点开始遍历更新 const reversedPath = path.reverse() as ReversePath<DeepNumberPath<T>>; const updatedEntity = updateNestedField(targetEntity, reversedPath); // 返回全新的Collection实例 return { ...collection, entities: { ...collection.entities, [entityKey]: updatedEntity } }; }
第三步:使用示例和类型安全验证
我们用一个具体的User实体来测试这个函数,看看类型安全的效果:
// 示例实体:继承自Entity,包含多层嵌套的数字字段和非数字字段 interface User extends Entity { stats: { posts: number; likes: number; profile: { views: number; }; }; name: string; // 非数字类型,路径会被TypeScript拦截 } // 创建测试用的Collection const userCollection: Collection<User> = { entities: { 'user-1': { id: 'user-1', stats: { posts: 5, likes: 20, profile: { views: 100 } }, name: 'Alice' } }, ids: ['user-1'] }; // ✅ 合法调用:路径指向数字类型字段,编译通过 const updatedPosts = increment(userCollection, 'user-1', ['stats', 'posts']); console.log(updatedPosts.entities['user-1'].stats.posts); // 输出6 // ✅ 合法调用:深层嵌套的数字字段路径 const updatedViews = increment(userCollection, 'user-1', ['stats', 'profile', 'views']); console.log(updatedViews.entities['user-1'].stats.profile.views); // 输出101 // ❌ 类型错误:路径指向非数字类型(name是string),编译期直接报错 // increment(userCollection, 'user-1', ['name']);
这个实现完全满足你的需求:它会自动校验路径对应的字段是否为number类型,同时通过不可变更新的方式符合接口的readonly约束。
内容的提问来源于stack exchange,提问作者Tarlen
相关产品推荐
相关产品推荐

