news 2026/10/1 2:05:17

type-challenges 实战:用模板字面量类型实现 Typed Get,为嵌套路径访问补全类型推断

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
type-challenges 实战:用模板字面量类型实现 Typed Get,为嵌套路径访问补全类型推断
  • 示例工程

【免费下载链接】type-challenges

Collection of TypeScript type challenges with online judge

项目地址:https://gitcode.com/GitHub_Trending/ty/type-challenges
点击查看免费下载

Typed Get(编号 270,难度 hard)是 type-challenges 仓库中的一道经典类型体操题:lodash 的get在 JavaScript 中访问嵌套值十分便捷,但进入 TypeScript 后函数签名会丢掉路径对应的类型信息;本题目要求借助 TypeScript 4.1 的 Template Literal Types(模板字面量类型),实现一个能在编译期根据点分路径解析出目标类型的Get<T, K>。读完本文,你将掌握模板字面量类型的模式匹配、条件类型分发与递归类型拆分的组合用法,并理解“字面量键优先于路径拆分”这一关键边界设计。

题目背景:lodash get 与类型信息的丢失

lodash 的get函数允许以字符串路径访问嵌套值,例如get(data, 'foo.bar.count')。它在 JavaScript 层面非常实用,但问题在于:get的运行时实现无法把路径字符串映射为 TypeScript 类型,因此这类 API 的返回值往往退化为any或宽泛的联合类型,嵌套对象原本精确的结构信息在调用点全部丢失。

这道题的核心诉求正是“夺回”这些类型信息。只要路径字符串是可静态分析的字面量类型(如'foo.bar.count'),就能通过模板字面量类型在类型层面把它逐段拆分,再结合索引访问T[K]递归下钻,最终在编译期推导出目标节点的精确类型。这也是模板字面量类型在“字符串即数据”场景中最典型的一类应用——把字符串当作一条结构化路径来解析。

题面给出了目标行为示例:

type Data = { foo: { bar: { value: 'foobar'; count: 6; }; included: true; }; hello: 'world'; }; type A = Get<Data, 'hello'>; // 'world' type B = Get<Data, 'foo.bar.count'>; // 6 type C = Get<Data, 'foo.bar'>; // { value: 'foobar', count: 6 }

并明确说明:本题不需要处理数组下标访问(题面原文“この課題では、配列へのアクセスは必要ありません”),路径仅由对象属性名以.连接而成。

题目信息与初始模板

该题在仓库中位于questions/00270-hard-typed-get/目录,从 info.yml 可以确认它的元数据:

  • 难度:hard
  • 标签:utils、template-literal(工具类 + 模板字面量主题)
  • 作者:Anthony Fu

template.ts 中给出的待填空模板极其精简:

type Get<T, K> = string

也就是说,起点是一个把所有输入都映射为string的占位实现,你需要把它重写为能够按路径推导类型的版本。

测试用例拆解:比题面更严苛的边界

题面只给了三条正向示例,但真正的约束藏在 test-cases.ts 里。它使用@type-challenges/utils(仓库内的 utils/index.d.ts)提供的Equal与Expect做类型级断言,共五条用例:

type cases = [ Expect<Equal<Get<Data, 'hello'>, 'world'>>, Expect<Equal<Get<Data, 'foo.bar.count'>, 6>>, Expect<Equal<Get<Data, 'foo.bar'>, { value: 'foobar', count: 6 }>>, Expect<Equal<Get<Data, 'foo.baz'>, false>>, Expect<Equal<Get<Data, 'no.existed'>, never>>, ]; type Data = { foo: { bar: { value: 'foobar' count: 6 } included: true } 'foo.baz': false hello: 'world' }

注意这里比题面多了两条关键约束:

  1. Get<Data, 'foo.baz'>必须返回false。Data中恰好存在一个字面量键'foo.baz'(键名本身包含点号),其值为false。这条用例要求:当完整路径本身就是一个真实键时,必须优先按字面量键直接索引,而不能把它当作foo→baz的嵌套路径去拆分(否则会走Data['foo']之后找不到baz,结果退化为never)。
  2. Get<Data, 'no.existed'>必须返回never。对于完全不存在(既不是字面量键、也无法逐段命中)的路径,结果应为never,而不是报错或返回any。这要求实现对“路径失效”有明确的兜底分支。

核心实现:递归拆分 + 索引访问

把Get写成一个先查整键、再递归拆分的类型,即可同时满足上述全部用例:

type Get<T, K extends string> = K extends keyof T ? T[K] : K extends `${infer Head}.${infer Tail}` ? Head extends keyof T ? Get<T[Head], Tail> : never : never

逐行解读这条类型的执行逻辑:

  • 第一分支K extends keyof T:先检查完整路径字符串是否直接就是T的某个键。这覆盖了'hello'、'foo.bar'(字面量键)等“整串命中”的情况——尤其是'foo.bar'这条用例,正是靠这个分支把结果锁定为false,而不是错误地进入拆分逻辑。
  • **第二分支K extends \${infer Head}.${infer Tail}`**:当整串不是键时,利用模板字面量类型的模式匹配,把K在第一个.处拆为Head(首段键名)与Tail(剩余路径)。例如'foo.bar.count'会拆出Head = 'foo'、Tail = 'bar.count'`。
  • Head extends keyof T ? Get<T[Head], Tail> : never:首段必须命中当前对象的键;命中则对T[Head]用剩余路径Tail递归调用Get,实现逐层下钻;未命中则说明路径在某一层断掉了,返回never。
  • 最终兜底: never:如果K既不是键、也拆不出.,说明它是不存在的孤立键名,同样返回never。

用五条用例验证一遍:

输入路径执行轨迹结果
'hello'命中整键 →Data['hello']'world'
'foo.bar.count'拆分 →Data['foo']→ 递归拆'bar.count'→Data['foo']['bar']→ 递归'count'→ 整键命中6
'foo.bar'整键不命中 → 拆分'foo'+'bar'→ 递归Get<bar对象, 'bar'>→ 整键命中{ value: 'foobar', count: 6 }
'foo.baz'整键命中(字面量键优先)→Data['foo.baz']false
'no.existed'整键不命中 → 拆分'no'+'existed'→'no'不是键 → 兜底never

关键设计点在于:整键检查必须放在模式匹配拆分之前。若颠倒顺序,'foo.baz'会先被拆成'foo'与'baz',随后在Data['foo'](一个不含baz的对象)中找不到'baz'而返回never,与测试用例Expect<Equal<Get<Data, 'foo.baz'>, false>>直接冲突。这一“字面量键优先”的顺序,本质上是把字符串键的两种语义——字面量键名与路径表达式——做了明确的优先级约定。

实现变体:用默认泛型参数强化边界

另一个常见变体是利用泛型默认参数,在递归时把失败信号显式传递:

type Get<T, K extends string, Fallback = never> = K extends keyof T ? T[K] : K extends `${infer Head}.${infer Tail}` ? Head extends keyof T ? Get<T[Head], Tail, Fallback> : Fallback : Fallback

这个版本把“路径失效”的兜底值抽象成第三个泛型参数(默认never),行为与标准解法一致;若将来想“路径不存在时返回用户指定的默认值”,只需显式传入第三个参数即可,可读性略好但泛化程度更高。两种写法都能通过test-cases.ts的全部断言。

底层原理:模板字面量类型的模式匹配

这道题能成立,完全依赖 TypeScript 4.1 引入的模板字面量类型。其核心能力是:在K extends \${infer Head}.${infer Tail}`这样的条件类型中,编译器会把字面量字符串按模板结构进行**模式匹配与推断**——.前的内容推断为Head,.后的内容推断为Tail,从而把运行时字符串的“split 逻辑”搬到了类型层面。这正是题目标签template-literal` 的含义。

配合使用的另外两个机制是:

  • 条件类型分发:K extends ... ? ... : ...会基于真实字符串字面量逐条计算,保证路径是精确的字面量类型而非宽泛的string(若K是string,推断出的Head/Tail也会退化为string,无法索引)。
  • 递归类型引用:Get<T[Head], Tail>允许类型在自身定义中引用自己,把“逐段下钻”表达为栈式递归;每次递归减少一段路径,最终收敛到整键命中的基线分支。本仓库中 guides/recursive.md 与 guides/key-in.md 也分别以 TODO 形式预留了递归与键操作专题的扩展资料,可见这两个主题是该系列挑战反复出现的核心技法。

需要留意的是,递归类型依赖编译器设定的类型实例化深度上限(默认约 50 层),因此对于超深层级对象或超长路径,Get存在递归深度限制——这是所有基于递归拆分的类型体操共同的约束,对本题的正常用例(三层嵌套)完全够用。

总结

Typed Get的价值在于把 lodashget丢失的类型信息重新“焊接”回调用点:通过模板字面量类型的模式匹配把点分路径逐段拆分,借助条件类型的整键优先分支处理“键名含点”的边界情况,再用递归类型逐层下钻直至命中目标类型。最终产物是一个由 template.ts 中一行占位type Get<T, K> = string演化而来的精确路径索引器,且全部行为都有 test-cases.ts 中的Equal/Expect断言背书。

若想亲手验证或继续深入,可基于 test-cases.ts 中的Data结构替换为自己的嵌套对象,观察不同路径分支的推导结果;进一步地,还可以把Get的兜底值泛型化、扩展数组下标访问(题面明确不要求,但可作为延伸练习)、或与Partial、Readonly等工具类型组合,构建更完整的类型安全数据访问层。

  • 示例工程

【免费下载链接】type-challenges

Collection of TypeScript type challenges with online judge

项目地址:https://gitcode.com/GitHub_Trending/ty/type-challenges
点击查看免费下载
上一篇:llama.cpp 移动端部署:4 步把大模型搬进 Android 与 iOS
下一篇:pidcat完全教程:从安装到高级过滤的完整指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 2:04:03

Steam停止支持Windows 7:原因解析与老电脑的生存指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 2:03:48

使用 flow-graph-mcp-server 以 AI 驱动方式构建 Babylon.js Flow Graph

图形学游戏开发3D渲染 【免费下载链接】Babylon.js Babylon.js is a powerful, beautiful, simple, and open game and rendering engine packed into a friendly JavaScript framework. 项目地址&#xff1a; https://gitcode.com/gh_mirrors/ba/Babylon.js 点击查看 免费下载…

作者头像 李华
网站建设 2026/10/1 2:03:20

路径分析实战:结构方程模型、效应拆解与Python实现

简介&#xff1a;面向社会科学、计量经济学等领域研究者及Python初学者&#xff0c;这份路径分析&#xff08;Path Analysis&#xff09;代码用于构建结构方程模型并量化变量间因果关系&#xff0c;写法简洁、结果直观。项目基于回归分析计算路径系数&#xff0c;借助NetworkX绘…

作者头像 李华
网站建设 2026/10/1 2:02:44

越省越费?杰文斯悖论揭示效率提升背后的能耗反弹

先别急着把这篇文章关了。我一开始看到“Jev”这个词也懵&#xff1a;这到底是个库&#xff1f;是个算法&#xff1f;还是某位网友的外号&#xff1f;后来翻了上下文才发现&#xff0c;大家口中的 Jev&#xff0c;大概率是 Jevons Paradox&#xff08;杰文斯悖论&#xff09;的…

作者头像 李华