基于嵌套键数组的TypeScript返回类型实现求助
解决方案
1. 基础类型定义
先明确实体类型,将未通过include获取的属性标记为可选:
type Brand = { id: string; name: string; }; type Product = { id: string; title: string; price: number; }; type CartItem = { id: string; quantity: number; brand?: Brand; // 未include时可选 product?: Product; // 未include时可选 }; type Cart = { id: string; total: number; items?: CartItem[]; // 未include时可选 };
2. 核心类型工具
实现递归类型工具,解析include路径并将对应嵌套属性转为必选:
// 单一路径转换:将对象指定路径的属性转为必选 type DeepRequiredByPath<T, Path extends string> = Path extends `${infer Key}.${infer Rest}` ? Key extends keyof T ? { [K in keyof T]: K extends Key ? T[K] extends Array<infer U> ? Array<DeepRequiredByPath<U, Rest>> // 处理数组嵌套场景 : DeepRequiredByPath<NonNullable<T[K]>, Rest> // 处理对象嵌套场景 : T[K] } & Required<Pick<T, Key>> // 标记当前层级属性为必选 : T : Path extends keyof T ? Required<Pick<T, Path>> & Omit<T, Path> // 顶级属性直接转为必选 : T; // 多路径合并:依次应用所有路径的转换逻辑 type DeepRequiredByPaths<T, Paths extends string[]> = Paths extends [infer First, ...infer Rest] ? First extends string ? Rest extends string[] ? DeepRequiredByPaths<DeepRequiredByPath<T, First>, Rest> : T : T : T;
3. 约束include参数(可选但推荐)
添加类型约束,确保用户只能传入实体的有效嵌套路径,避免无效输入:
// 生成实体的所有有效嵌套路径 type ValidPaths<T, Prefix extends string = ''> = { [K in keyof T]: T[K] extends Array<infer U> ? `${Prefix}${K}` | ValidPaths<U, `${Prefix}${K}.`> : T[K] extends object ? `${Prefix}${K}` | ValidPaths<T[K], `${Prefix}${K}.`> : `${Prefix}${K}` }[keyof T];
4. 实现retrieve函数
利用上述类型工具,让函数根据include参数自动推断返回类型:
async function retrieve<Include extends ValidPaths<Cart>[] = []>( include?: Include ): Promise<DeepRequiredByPaths<Cart, Include>> { // 实际API请求逻辑示例 const response = await fetch('/api/cart', { method: 'GET', headers: { 'Content-Type': 'application/json' }, body: include ? JSON.stringify({ include }) : undefined }); return response.json(); }
5. 类型校验测试
以下场景会自动触发TypeScript类型校验:
(async () => { // 无include:items为可选属性 const cart1 = await retrieve(); cart1.items; // 类型:CartItem[] | undefined // include ["items"]:items转为必选,但其子属性仍为可选 const cart2 = await retrieve(["items"]); cart2.items; // 类型:CartItem[](必选) cart2.items[0].brand; // 类型:Brand | undefined // include ["items.brand", "items.product"]:子属性也转为必选 const cart3 = await retrieve(["items.brand", "items.product"]); cart3.items[0].brand.name; // 类型安全,无报错 cart3.items[0].product.price; // 类型安全,无报错 // 传入无效路径会直接报错:类型'"invalid.path"'不能赋值给ValidPaths<Cart>类型 // const cart4 = await retrieve(["invalid.path"]); })();
内容的提问来源于stack exchange,提问作者Axel DS.
相关产品推荐
相关产品推荐

