- 示例工程
【免费下载链接】type-challenges
Collection of TypeScript type challenges with online judge
本篇指南基于 type-challenges 仓库中的00014-easy-first挑战(题目原文),围绕"如何用 TypeScript 类型系统从数组/元组中提取首元素类型"展开。你将掌握First<T>泛型的约束写法、索引访问类型(Indexed Access)的使用、空数组边界处理,并通过仓库自带的测试用例验证答案的正确性。完成此题后,你还可以顺势挑战其姊妹题 Last of Array,进一步巩固元组模式匹配的思维。
一、题目概述:从一个数组提取首元素类型
本挑战属于 type-challenges 的easy(쉬움)难度关卡,核心标签为array(详见 info.yml),作者是 Anthony Fu(@antfu),对应题号14。
题目要求(原文为韩文,见 README.ko.md):
实现一个接收数组(元组)
T并返回其第一个元素类型的泛型First<T>。
示例:
type arr1 = ['a', 'b', 'c'] type arr2 = [3, 2, 1] type head1 = First<arr1> // expected to be 'a' type head2 = First<arr2> // expected to be 3从这个例子可以看出,First<T>不是取数组的"运行时首元素",而是在类型层面推导出元组的首元素类型:对['a', 'b', 'c']得到字面量类型'a',对[3, 2, 1]得到字面量类型3。
二、起点模板与题目约束
在动手之前,先看仓库中提供的起点模板 template.ts:
type First<T extends any[]> = any模板已经把两件事交代清楚了:
- 泛型参数必须被约束为数组:
T extends any[]。这保证了传入First的一定是数组或元组类型,而不是字符串、对象等。 - 返回值处是
any:这是留给解题者填充的空位,需要替换为真正提取首元素类型的逻辑。
也就是说,答案的核心任务只有一个:把any替换为"取T的第一个元素类型"的类型表达式。
三、解法一:通过索引访问类型 T[0]
最直接的思路是使用 TypeScript 的索引访问类型(Indexed Access Type):
type First<T extends any[]> = T[0]对于元组['a', 'b', 'c'],T[0]会精确得到首元素类型'a';对于[3, 2, 1]得到3。这正是题目示例期望的结果。
需要注意的一个边界场景是空元组:First<[]>中,T[0]无法命中任何元素,TypeScript 会将其推导为never。这恰好与仓库测试用例 test-cases.ts 中的断言一致:
Expect<Equal<First<[]>, never>>,所以T[0]这一写法天然覆盖了空数组场景,无需额外处理。
四、解法二:通过 infer 模式匹配提取首元素
另一种常见思路是利用条件类型 +infer从元组中"拆出"首元素:
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never它的工作原理是:
T extends [infer F, ...any[]]将T与"至少含一个元素的元组"进行模式匹配;- 匹配成功时,
F被推断为元组的首元素类型,直接返回F; - 匹配失败(即空元组
[])时,走never分支。
两种写法在本题的测试用例下结果等价,T[0]更简洁,infer写法则更贴近后续 medium/hard 关卡中频繁使用的"元组拆分"思维(例如 Last of Array 就需要用到[...infer Rest, infer L]的形式)。
五、用仓库测试用例验证答案
type-challenges 的每个关卡都配有test-cases.ts作为在线判题依据。本题的测试用例包含 4 条正向断言与 2 条负向断言:
import type { Equal, Expect } from '@type-challenges/utils' type cases = [ Expect<Equal<First<[3, 2, 1]>, 3>>, Expect<Equal<First<[() => 123, { a: string }]>, () => 123>>, Expect<Equal<First<[]>, never>>, Expect<Equal<First<[undefined]>, undefined>>, ] type errors = [ // @ts-expect-error First<'notArray'>, // @ts-expect-error First<{ 0: 'arrayLike' }>, ]这些用例从多个维度约束了答案的正确性:
- 函数类型作为元素:
First<[() => 123, { a: string }]>必须精确返回函数类型() => 123,说明首元素可以是任意类型,不能被宽化成Function; - 空数组边界:
First<[]>必须是never; undefined也是合法元素:First<[undefined]>应返回undefined类型本身;- 负向约束:
First<'notArray'>和First<{ 0: 'arrayLike' }>必须报错——这正是T extends any[]约束发挥的作用,它阻止了把字符串、对象等非数组类型传入First。注意{ 0: 'arrayLike' }虽然按索引可以访问到'arrayLike',但它不是数组,因此同样被拒绝。
@type-challenges/utils是仓库 workspace 内的工具包,其中 utils/index.d.ts 定义了Equal和Expect等断言工具。Equal<X, Y>采用函数参数逆变比较的经典技巧实现严格相等判断,只有两侧类型完全一致时才返回true,因此你的答案必须与期望类型精确相等,不能只是"兼容"。
六、本地运行与在线挑战方式
本仓库使用 pnpm 管理依赖(见根目录 package.json,包管理器为pnpm@8.12.1)。如果你想在本地体验在线判题,需要先安装依赖:
pnpm install之后可以直接在编辑器中打开questions/00014-easy-first/template.ts,填入答案后通过 TypeScript 类型检查(例如npx tsc --noEmit)验证测试是否通过。
更推荐的方式是直接访问该题的在线 Playground(README.md 中提供了挑战入口),在浏览器里即时验证答案,并可以查看社区提交的多种解题思路。
七、延伸:姊妹题 Last of Array
本关卡在 info.yml 中声明了关联题15,即 Last of Array(medium 难度)。它的模板是:
type Last<T extends any[]> = any取最后一个元素无法再用T[0]这种固定索引,而需要配合infer:
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never可以看出,First是理解"元组 + infer 模式匹配"的入门题:先掌握[infer F, ...any[]]从头部拆解,再学会[...any[], infer L]从尾部拆解,后续的 Pop、Shift 等数组操作挑战就都有了基础。
小结
First<T>看似简单,却浓缩了类型挑战的三项基本功:用extends any[]约束泛型参数、用T[0]或infer从元组中提取元素类型、用never处理空数组边界。对照 template.ts 与 test-cases.ts 完成并验证答案,你就迈出了 type-challenges 数组系列类型编程的第一步。
- 示例工程
【免费下载链接】type-challenges
Collection of TypeScript type challenges with online judge
相关推荐
CUDA 加速 Black-Scholes 期权定价:cuda-samples BlackScholes 示例源码级解析
CUDA 加速 Black Scholes 期权定价:cuda samples BlackScholes 示例源码级解析 导读 本文基于 NVIDIA CUDA
示例工程TypeScript 类型挑战 00014:实现 First\<T\> 获取数组第一个元素类型
TypeScript 类型挑战 00014:实现 First\<T\ 获取数组第一个元素类型 本篇文章围绕 type challenges 仓库中编号 0001
示例工程type-challenges 第 15 题「最后一个元素 Last of Array」:用类型体操实现数组尾元素提取
type challenges 第 15 题「最后一个元素 Last of Array」:用类型体操实现数组尾元素提取 type challenges 是一个带
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考