如何通过字符串路径让TypeScript推断对象非空值?
如何通过字符串路径让TypeScript推断对象属性非空?
问题背景
假设有一个键类型可为任意类型(包括null和undefined)的动态对象,需要实现一个函数:接收该对象的字符串路径数组,当所有路径对应的对象值均不为null或undefined时返回true。目标是让TypeScript自动推断这些路径对应值非空,从而无需使用!非空断言操作符。
示例代码如下:
interface Foo { a?: { i?: number; ii?: number }, b?: string; c?: {} } var object: Foo = { a: { i: undefined, ii: 1 }, b: undefined, c: {} } if (exists(object, ["a.ii", "c"])) // 当`object.a.ii`和`object.c`非null/undefined时返回true { // 期望TypeScript推断`object.a.ii`非空,无需使用`!` let value: number = object.a.ii!; }
已知单个值的非空推断可以这样实现:
function exists<T>(object: T | null | undefined): object is T { return object !== null && object !== undefined; } if (exists(object.a.ii) && exists(object.c)) { let value: number = object.a.ii; // 无需`!`,可正常运行 }
现在需要知道能否通过字符串路径的方式实现上述非空推断效果。
解决方案
可以实现,核心是利用TypeScript的条件类型和模板字面量类型解析路径,结合类型谓词来缩小对象的类型范围。
1. 定义路径解析工具类型
首先需要工具类型将字符串路径拆解为属性层级,并获取对应属性类型:
// 拆分字符串路径为属性数组类型 type SplitPath<S extends string> = S extends `${infer Head}.${infer Tail}` ? [Head, ...SplitPath<Tail>] : [S]; // 根据属性数组获取对应层级的类型 type GetPathType<T, Path extends string[]> = Path extends [infer First extends keyof T, ...infer Rest extends string[]] ? GetPathType<T[First], Rest> : T;
2. 定义非空标记工具类型
接下来实现工具类型,将指定路径的所有节点标记为非null/undefined:
// 递归标记单一路径上的属性为非空 type MarkPathAsNonNullable<T, Path extends string[]> = Path extends [infer First extends keyof T, ...infer Rest extends string[]] ? T extends null | undefined ? never // 若当前节点为空,直接返回never,避免后续无效访问 : { [K in keyof T]: K extends First ? MarkPathAsNonNullable<NonNullable<T[K]>, Rest> : T[K] } : NonNullable<T>; // 批量处理多个路径的类型标记 type MarkPathsAsNonNullable<T, Paths extends string[]> = Paths extends [infer P extends string, ...infer Rest extends string[]] ? MarkPathsAsNonNullable<MarkPathAsNonNullable<T, SplitPath<P>>, Rest> : T;
3. 实现带类型推断的exists函数
最后编写exists函数,结合运行时检查和类型谓词:
function exists<T, Paths extends string[]>( obj: T, paths: Paths ): obj is MarkPathsAsNonNullable<T, Paths> { // 运行时遍历所有路径,逐一检查属性是否非空 return paths.every(path => { let current: any = obj; const keys = path.split('.'); for (const key of keys) { if (current === null || current === undefined) return false; current = current[key]; } return current !== null && current !== undefined; }); }
测试使用
用开头的示例验证效果:
interface Foo { a?: { i?: number; ii?: number }, b?: string; c?: {} } var object: Foo = { a: { i: undefined, ii: 1 }, b: undefined, c: {} } if (exists(object, ["a.ii", "c"])) { // TypeScript自动推断object.a.ii为number,object.c为{},无需非空断言 let value: number = object.a.ii; let cValue: {} = object.c; // 错误示例:如果访问未检查的路径,仍会报错 // let bValue: string = object.b; // 类型错误:object.b可能为undefined }
限制说明
- 仅支持静态字面量路径,动态生成的字符串路径无法被TypeScript编译时解析,因此无法触发类型推断。
- 如果路径中的中间节点本身是
null/undefined,类型会被推断为never,对应的if分支会被TypeScript判定为不可达,符合运行时逻辑。
内容的提问来源于stack exchange,提问作者匿名
相关产品推荐
相关产品推荐

