如何将递归类型用作函数返回类型?以safeSetIn函数为例
问题分析与解决方案
递归类型完全可以作为函数返回类型,你遇到的问题是函数的泛型参数约束和返回类型的关联逻辑没处理到位,导致TypeScript没有触发MySet类型的never分支。
核心原因
单独使用MySet<{a: string}, ['a', 'b'], 'test2'>时,TypeScript能直接解析出路径无效,返回never;但放到safeSetIn函数中时,若没有对键路径Path做有效约束,TypeScript会默认允许任意PropertyKey数组作为路径,此时不会主动触发MySet的never分支,反而会根据函数实现的返回值(原对象的拷贝)推导返回类型为原对象类型。
修复方案
1. 完善路径有效性校验类型
先定义一个辅助类型,用来判断键路径是否为目标对象的有效路径:
type ValidPath<T, Path extends PropertyKey[]> = Path extends [infer First, ...infer Rest extends PropertyKey[]] ? First extends keyof T ? ValidPath<T[First], Rest> : never : T;
2. 重新约束函数的泛型参数
修改safeSetIn函数的泛型定义,通过辅助类型ValidPath触发无效路径的never分支,同时约束路径参数的类型:
// 假设你的MySet递归类型实现如下 type MySet<T, Path extends PropertyKey[], V> = Path extends [infer First, ...infer Rest extends PropertyKey[]] ? First extends keyof T ? Rest extends [] ? Omit<T, First> & { [K in First]: V } : Omit<T, First> & { [K in First]: MySet<T[First], Rest, V> } : never : T; function safeSetIn< T extends object, Path extends PropertyKey[], V, // 用辅助类型校验路径有效性,无效时返回never PathCheck = ValidPath<T, Path> >( obj: T, // 路径无效时,参数类型变为never,直接在编译期报错 path: PathCheck extends never ? never : Path, value: V ): MySet<T, Path, V> { // 运行时实现:严格校验路径有效性 const result = {...obj} as any; let current = result; for (let i = 0; i < path.length; i++) { const key = path[i]; if (i === path.length - 1) { current[key] = value; break; } if (!(key in current)) { throw new Error(`Invalid path: ${path.join('.')}`); } current = current[key]; } return result as MySet<T, Path, V>; }
3. 测试验证
const mock = {a: 'test'}; // 编译期直接报错(路径无效),返回类型推导为never const result = safeSetIn(mock, ['a', 'b'], 'test2'); // 有效路径的情况,返回正确的类型 const validResult = safeSetIn(mock, ['a'], 'test2'); // validResult类型等价于 { a: string }
关键总结
- 递归类型作为函数返回类型完全可行;
- 必须通过泛型约束让TypeScript明确识别无效路径,才能触发
MySet的never分支; - 同时要在运行时添加路径校验,保证类型安全和运行时安全。
内容的提问来源于stack exchange,提问作者Dan Nugent
相关产品推荐
相关产品推荐

