如何让TypeScript中读取对象路径的getIn函数实现类型安全?
实现类型安全的对象路径读取辅助函数
我想要创建一个读取对象路径的简易辅助函数,示例如下:
interface Human { address: { city: { name: string; } } } const human: Human = { address: { city: { name: "Town"}}}; getIn<Human>(human, "address.city.name"); // 返回"Town"
该辅助函数在JavaScript中很容易实现,但在TypeScript中实现类型安全则较为复杂。我目前的实现如下:
type Prev = [never, 0, 1, 2, 3, 4, 5, 6, ...0[]]; type Join<K, P> = K extends string | number ? P extends string | number ? `${K}${"" extends P ? "" : "."}${P}` : never : never; type Path<T, D extends number = 4> = [D] extends [never] ? never : T extends object ? { [K in keyof T]-?: K extends string | number ? `${K}` | Join<K, Path<T[K], Prev[D]>> : never; }[keyof T] : ""; function getIn<T extends Record<string, any>>(object: T, path: Path<T>): T { const parts = path.split("."); return parts.reduce<T>((result, key) => { if (result !== undefined && result[key]) { return result[key]; } return undefined; }, object); }
这段代码可以运行,但存在问题:getIn函数的返回类型不应为T,而应根据传入的路径返回对象T内部的对应类型。例如调用getIn<Human>(human, "address.city.name")时,TypeScript应识别返回值为string;传入"address.city"时,返回类型应为对应的{ name: string }类型等。请问有什么方法可以实现该函数的类型安全?
解决方案:实现路径对应的类型提取
要让getIn返回正确的类型,我们需要新增一个PathValue类型,用来根据传入的对象类型T和路径P,递归提取最终的目标类型。同时需要调整getIn的泛型定义,让它关联路径和返回类型:
完整实现代码
// 用于递归深度递减的辅助类型 type Prev = [never, 0, 1, 2, 3, 4, 5, 6, ...0[]]; // 拼接路径片段的辅助类型 type Join<K, P> = K extends string | number ? P extends string | number ? `${K}${"" extends P ? "" : "."}${P}` : never : never; // 生成对象所有合法路径的类型 type Path<T, D extends number = 4> = [D] extends [never] ? never : T extends object ? { [K in keyof T]-?: K extends string | number ? `${K}` | Join<K, Path<T[K], Prev[D]>> : never; }[keyof T] : ""; // 根据路径提取对应值的类型 type PathValue<T, P extends Path<T>> = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? Rest extends Path<T[K]> ? PathValue<T[K], Rest> : never : never : P extends keyof T ? T[P] : never; // 类型安全的getIn函数 function getIn<T extends Record<string, any>, P extends Path<T>>(object: T, path: P): PathValue<T, P> | undefined { const parts = path.split("."); return parts.reduce((result, key) => { if (result === undefined || result === null) { return undefined; } return result[key as keyof typeof result]; }, object as any) as PathValue<T, P> | undefined; }
关键改进点说明
新增
PathValue类型:- 通过字符串字面量拆分(
infer关键字),将路径拆分为当前键K和剩余路径Rest - 递归遍历对象结构,最终提取路径对应的目标类型
- 处理了路径为单个键和多层嵌套两种情况
- 通过字符串字面量拆分(
调整
getIn泛型:- 新增泛型参数
P extends Path<T>,绑定路径类型和对象类型 - 返回类型改为
PathValue<T, P> | undefined,明确可能返回undefined的情况(当路径不存在或中间节点为null/undefined时)
- 新增泛型参数
修复运行时逻辑:
- 增加对
null的判断,避免运行时错误 - 使用类型断言处理
reduce中的键类型问题,确保运行时兼容性
- 增加对
测试验证
interface Human { address: { city: { name: string; } }; age?: number; } const human: Human = { address: { city: { name: "Town" } }, age: 25 }; // 返回类型为string,值为"Town" const cityName = getIn(human, "address.city.name"); // 返回类型为{ name: string },值为{ name: "Town" } const city = getIn(human, "address.city"); // 返回类型为number | undefined,值为25 const age = getIn(human, "age"); // 路径不存在时,返回类型为undefined const invalid = getIn(human, "address.zip");
扩展说明
- 递归深度:默认深度为4,可通过
Path<T, 6>这样的方式调整最大嵌套深度 - 可选属性:自动支持可选属性的路径读取,返回类型会包含
undefined - 数组支持:如果对象包含数组,可扩展类型支持数组索引(如
"users.0.name"),只需调整Path和PathValue类型对数字键的处理
内容的提问来源于stack exchange,提问作者Jarzka
相关产品推荐
相关产品推荐

