- 示例工程
【免费下载链接】type-challenges
Collection of TypeScript type challenges with online judge
本篇技术指南以 type-challenges 仓库中第 15 号中等难度题目(questions/00015-medium-last/)为核心,系统讲解如何实现一个接收数组/元组类型T、返回其最后一个元素的泛型Last<T>。你将掌握条件类型、infer推断、TypeScript 4.0 变长元组类型(variadic tuple types)的组合用法,理解空数组边界如何处理,并通过仓库内的模板与测试用例完成本地验证。
挑战速览:题目背景与运行环境
本题目录位于 questions/00015-medium-last/,元数据文件 info.yml 记录了题目的关键信息:
- 标题:Last of Array
- 作者:Anthony Fu(
@antfu) - 标签:
#array - 难度:medium(中等)
- 关联题目:14(First of Array)、16(Pop)
题目原文(韩文版见 README.ko.md,英文版见 README.md,另有 简体中文版)给出的要求非常明确:
实现一个接收数组
T并返回其最后一个元素的泛型Last<T>。
同时,题目明确建议本挑战使用 TypeScript 4.0。这是因为 4.0 引入了变长元组类型(variadic tuple types),正是本题最简洁解法的基石。此外,根据仓库根目录 README.md 的说明,所有挑战均需在strict模式下通过(Challenges work in the strict mode)。
题目给出的起始模板位于 template.ts,初始状态只有一个占位实现:
type Last<T extends any[]> = any而期望的行为,以题目示例为参考:
type arr1 = ['a', 'b', 'c'] type arr2 = [3, 2, 1] type tail1 = Last<arr1> // expected to be 'c' type tail2 = Last<arr2> // expected to be 1测试用例逐条解析
题目是否通过,完全由测试文件 test-cases.ts 中的类型级断言决定。该文件从@type-challenges/utils导入Equal与Expect两个工具类型,定义了 4 条用例:
import type { Equal, Expect } from '@type-challenges/utils' type cases = [ Expect<Equal<Last<[]>, never>>, Expect<Equal<Last<[2]>, 2>>, Expect<Equal<Last<[3, 2, 1]>, 1>>, Expect<Equal<Last<[() => 123, { a: string }]>, { a: string }>>, ]逐条解读如下:
| 用例 | 输入元组 | 期望结果 | 考察点 |
|---|---|---|---|
| 1 | [] | never | 空数组没有最后一个元素,应安全地回退为never |
| 2 | [2] | 2 | 单元素元组,首元素即末元素 |
| 3 | [3, 2, 1] | 1 | 常规多元素元组的尾部提取 |
| 4 | [() => 123, { a: string }] | { a: string } | 元素为函数类型与对象类型时同样适用 |
这里的Equal与Expect定义在 utils/index.d.ts 中。Expect<T extends true>通过约束要求传入的必须是字面量true,否则触发类型错误;Equal<X, Y>则借助"条件类型函数签名对比"这一技巧实现严格的类型相等判断(X extends Y且Y extends X的双向可赋值性对部分场景不够严格,而Equal能区分更多细节差异)。因此这 4 条用例共同约束了Last的精确返回类型,而不是"可赋值即可"的宽松校验。
值得注意的一个细节是:测试用例 1 明确要求Last<[]>返回never,这意味着你的实现必须处理空元组分支,不能对空元组直接报错或返回any。
三种主流解题方案
方案一:条件类型 +infer+ 变长元组(最直观,推荐)
type Last<T extends any[]> = T extends [...infer Rest, infer L] ? L : never这是本题公认最简洁的解法,也是 TypeScript 4.0 推荐用在本挑战的直接原因:
T extends any[]约束输入必须是数组或元组类型,保证后续模式匹配合法;[...infer Rest, infer L]是变长元组类型的典型用法:在元组末尾"占位"一个待推断的L,剩余部分整体捕获为Rest;- 当
T是空元组[]时,无法匹配[...infer Rest, infer L]的形态(末尾必须至少有一个元素),条件类型走false分支,返回never,恰好满足用例 1。
这种写法的语义与题目"返回最后一个元素"一一对应:L就是最后一个元素,直接作为结果返回。
方案二:利用length与索引访问(无递归、无 infer)
type Last<T extends any[]> = [never, ...T][T['length']]思路是利用"前插一个占位元素,把末元素的下标整体后移一位":
- 对于
T = [3, 2, 1],T['length']是字面量3; [never, ...T]得到[never, 3, 2, 1],其中索引3恰好指向原元组的最后一个元素1;- 对于
T = [],[never, ...T]是[never],T['length']为0,取[never][0]得到never。
这个方案巧妙绕开了"元组索引最大为length - 1、而类型层无法做减法运算"的问题,用前插never的方式把索引换算成length。它的局限在于:当T不是元组而是普通数组类型(如number[])时,T['length']是number而非字面量,此时结果会退化为[never, ...T][number]这种联合/泛化形式,无法精确表达"最后一个元素"。因此该方案更适合输入确定为元组的场景。
方案三:递归剥离首元素
type Last<T extends any[]> = T extends [infer Head, ...infer Tail] ? Tail extends [] ? Head : Last<Tail> : never这是"从头到尾"的递归思路:每次匹配出首元素Head与剩余元组Tail;若Tail已为空,说明Head就是最后一个元素;否则对Tail递归调用自身。空元组同样在首层匹配失败,回退never。
与方案一相比,方案三更接近函数式编程的"逐层递推",适合作为理解递归类型如何逐步"吃掉"元组的练习,也方便读者把它推广到Pop、Shift、Reverse等后续题目。不过对于本题而言,方案一的代码量和可读性都更优。
关于T[number]的误区
初学者容易想到用T[number]取数组的元素类型,但这不能解决本题:T[number]对元组[3, 2, 1]求值得到的是所有元素组成的联合类型3 | 2 | 1,而非"最后一个元素"。它丢失了元组的位置信息,只有[...infer Rest, infer L]、T['length']这类保留"形状"的操作才能定位到尾部。
边界情况与扩展思考
空数组必须返回never
这是测试用例 1 的硬性要求。三种方案均通过条件类型的false分支天然满足了这一点,无需额外判断。
处理readonly元组
如果将来遇到readonly ['a', 'b']这类只读元组,方案一中的T extends any[]约束会匹配失败(readonly元组不能赋值给any[])。此时可以放宽约束为readonly any[]:
type Last<T extends readonly any[]> = T extends readonly [...infer Rest, infer L] ? L : never从当前测试用例看,test-cases.ts并未覆盖readonly场景,但这属于从实现结构可以推断出的自然延伸,在编写自己的工具类型时值得注意。
与前置题目 First of Array 的对比
本题与前一题 00014-easy-first(难度 easy)互为镜像。对比两个模板即可看出差异:
- 00014-easy-first/template.ts 的起始模板是
type First<T extends any[]> = any,目标是提取首元素; - 本题的目标是提取末元素。
First<T>的经典解法是T extends [infer F, ...infer Rest] ? F : never,即从元组头部捕获;而Last<T>则是从元组尾部捕获([...infer Rest, infer L])。两者的区别恰好体现了"变长元组类型中infer占位位置决定提取方向"这一核心思想。此外,00014-easy-first/test-cases.ts 中同样包含First<[]>返回never的用例,说明"空数组边界返回never"是这一系列题目的统一约定。
下一站:Pop
题目元数据还关联了 00016-medium-pop(难度 medium)。它的模板为type Pop<T extends any[]> = any,要求返回移除最后一个元素后的元组。掌握了[...infer Rest, infer L]之后,Pop的答案几乎呼之欲出——直接返回Rest即可。这也印证了本挑战在 "array" 主题下由易到难、循序渐进的设计思路:First(easy)→Last(medium)→Pop(medium)。
如何上手练习与本地验证
在线练习可直接在 TypeScript Playground 中体验。若希望在本地仓库环境验证,type-challenges 采用 pnpm workspace 管理,每个题目目录下均包含template.ts(待填充的实现)与test-cases.ts(类型断言)。test-cases.ts通过@type-challenges/utils(定义见 utils/index.d.ts,工具包说明见 utils/README.md)提供的Expect/Equal完成编译期校验:当你的Last<T>实现与用例期望不一致时,Expect<Equal<...>>会因为无法满足T extends true约束而在类型检查阶段直接报错。
你可以这样推进练习:
- 打开 template.ts,把
= any替换为上述任一实现; - 对照 test-cases.ts 中的 4 条用例逐个验证;
- 用题目示例中的
arr1/arr2自行补充断言,确认Last<['a', 'b', 'c']>精确等于'c'、Last<[3, 2, 1]>精确等于1; - 完成后可继续挑战 00016-medium-pop,检验对变长元组
infer模式的理解是否牢固。
小结
第 15 题Last<T>是理解 TypeScript 类型系统"模式匹配"能力的关键一题。通过它,你可以掌握三件事:条件类型 +infer的推断机制、TypeScript 4.0 变长元组类型的占位匹配,以及空数组边界统一返回never的约定。推荐优先掌握方案一(T extends [...infer Rest, infer L] ? L : never),它与前置题目First<T>的[infer F, ...infer Rest]形成对仗,是后续大量数组类工具类型(Pop、Shift、Reverse、LastIndexOf等)的公共基础。
- 示例工程
【免费下载链接】type-challenges
Collection of TypeScript type challenges with online judge
相关推荐
TypeScript 类型挑战 15:用 type-challenges 实现 `Last<T>` 提取数组最后一个元素
TypeScript 类型挑战 15:用 type challenges 实现 Last<T 提取数组最后一个元素 本篇围绕 type challenges 仓
示例工程type-challenges 第 15 题「最后一个元素 Last of Array」:用类型体操实现数组尾元素提取
type challenges 第 15 题「最后一个元素 Last of Array」:用类型体操实现数组尾元素提取 type challenges 是一个带
示例工程TypeScript 类型挑战 15:实现 `Last<T>` 泛型,提取数组最后一个元素类型
TypeScript 类型挑战 15:实现 Last<T 泛型,提取数组最后一个元素类型 导读 Last<T 是 type challenges 题库中的第 1
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考