- 示例工程
【免费下载链接】type-challenges
Collection of TypeScript type challenges with online judge
本篇文章围绕 type-challenges 仓库中的第 14 号入门挑战「First of Array」展开,完整讲解泛型First<T>的题目要求、约束写法、四种主流实现思路(索引访问、infer 推断、元组解构、条件类型),并结合仓库内模板与测试用例逐条验证答案的正确性。读完本文,你将掌握数组类型取首元素的类型级实现,并理解T extends any[]泛型约束、空数组返回never等边界处理。
题目速览
「First of Array」是 type-challenges 系列中编号 00014 的一道 easy(简单)级别挑战,主题标签为#array,由项目作者 Anthony Fu 编写。题目元数据记录在 info.yml 中:difficulty: easy、tags: array,并在related: 15字段中声明了与第 15 号挑战「Last of Array」的关联关系。
这道题位于整个系列早期(00013 之后),是学习泛型约束与数组类型索引最合适的入门题之一。它的英文原题与简体中文译文分别位于 README.md 与 README.zh-CN.md,并配有日文(README.ja.md)与韩文(README.ko.md)翻译,便于不同语言背景的读者对照学习。
题目要求
原题描述非常精炼:
Implement a generic
First<T>that takes an ArrayTand returns its first element's type.
即:实现一个泛型First<T>,它接收一个数组类型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是类型层面的操作:arr1是字符串字面量元组类型['a', 'b', 'c'],First<arr1>的结果是字面量类型'a';First<arr2>的结果是数字字面量类型3。这与运行时arr[0]的行为一一对应,但发生在编译期。
模板与起点:一个带约束的占位符
仓库中每个挑战都提供一个待完成的模板文件,本题的模板位于 template.ts:
type First<T extends any[]> = any模板已经帮我们完成了两件事:
- 声明了泛型参数
T,等待使用者基于它编写类型逻辑; - 预先写好了约束
T extends any[],明确要求T必须是数组类型(any[]表示"任意元素类型的数组")。
这个约束不是可有可无的:它保证了后续所有基于数组特性的操作(索引、infer、解构)在类型层面都是合法的。若去掉该约束,直接写type First<T> = T[0],那么当T传入非数组类型(如字符串、对象)时,TypeScript 会直接报错。
模板默认返回any,显然无法通过测试——我们的任务就是把这个占位符替换成真正能推导出首元素类型的实现。
解法一:索引访问T[0](最直观)
利用元组类型支持数字索引访问的特性,直接写:
type First<T extends any[]> = T[0]T[0]在类型层面等价于"取出元组的第 0 个元素的类型"。对于['a', 'b', 'c'],T[0]得到'a';对于[3, 2, 1],得到3。
这种写法最简洁,也是官方解法中最常见的一种。唯一的隐患在于空数组边界:当T为[]时,T[0]在 TypeScript 中会得到undefined(并非never),这与测试用例的期望不一致,详见下文「测试用例解析」。
解法二:infer 模式匹配
利用条件类型与infer关键字进行模式匹配:
type First<T extends any[]> = T extends [infer U, ...unknown[]] ? U : never其原理是:将元组T与模板类型[infer U, ...unknown[]]做匹配。若T是非空元组,则第一个元素被捕获到U中,...unknown[]匹配剩余任意数量的元素;若T为空元组[],匹配失败,走never分支。
infer是 TypeScript 条件类型中最强大的工具之一,本仓库还专门提供了 guides/infer.md 作为进阶阅读材料。后续的「Last of Array」「Pop」「Shift」等数组挑战,几乎都依赖这种"首元素 + 剩余部分"的拆分思路。
解法三:元组解构
利用泛型参数直接解构元组:
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never思路与解法二一致,只是占位符换成了any。这里的重点在于元组模式[infer F, ...any[]]能够自动处理空数组——当T = []时条件不成立,进入never分支,从而与测试期望对齐。
解法四:先判空再索引
将"判空"显式写出来:
type First<T extends any[]> = T['length'] extends 0 ? never : T[0]利用元组的length属性:T['length'] extends 0判断T是否为空元组。若为空则返回never,否则返回T[0]。这种写法把边界情况放在明面上,可读性最好,也最适合在面试或讲解中作为"逐步推导"的示范。
测试用例解析:四个正向用例 + 两个反向用例
仓库为每道题都配了完整的类型级测试,本题位于 test-cases.ts,导入自@type-challenges/utils的Equal与Expect工具:
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' }>, ]逐条拆解这些用例,能帮我们更完整地理解题目边界:
| 用例 | 输入 | 期望输出 | 考察点 |
|---|---|---|---|
| 1 | [3, 2, 1] | 3 | 基本场景:数字字面量元组取首元素 |
| 2 | [() => 123, { a: string }] | () => 123 | 复杂元素类型:函数类型也能被原样提取 |
| 3 | [] | never | 空数组边界:此时应返回never |
| 4 | [undefined] | undefined | 元素本身为undefined时也必须正确区分 |
第 3、4 条用例放在一起尤其值得注意:如果只用T[0],空数组会得到undefined,与用例 4 的[undefined]返回undefined混为一谈,无法通过测试。所以能区分[]与[undefined]的实现(infer、解构、先判空)才是完备的。
反向用例则验证了泛型约束的作用:
First<'notArray'>:字符串不是数组,必须报类型错误;First<{ 0: 'arrayLike' }>:即使对象带有数字索引键(类数组结构),也不允许通过——因为约束写死了T extends any[],只有真正的数组/元组类型才能作为参数。
反向用例通过// @ts-expect-error注释声明"此处应当产生类型错误",若实现没有正确约束泛型,这行注释反而会变成编译错误,测试即失败。
关于测试工具本身:Equal在 utils/index.d.ts 中定义,通过比较两个条件类型是否相互兼容来判断类型完全相等(实现基于(<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2)的经典技巧),而Expect<T extends true>则要求传入的类型必须是true,否则编译报错。二者组合,构成了 type-challenges 全系列统一使用的类型断言机制。
题目归属与整体结构
「First of Array」是 type-challenges 中数组主题挑战的第一站。从仓库目录结构可以看到,它的编号 00014 位于questions/00014-easy-first/,紧随其后的 00015-medium-last(Last of Array) 难度升至 medium,要求提取数组最后一个元素,需要用到T extends [...infer Rest, infer Last]这类反向拆分技巧,本质上是本题思路的镜像延伸。info.yml 中的related: 15字段正是对这一系列关系的机器可读记录。
此外,本仓库还收录了 00011-easy-tuple-to-object(元组转对象)、00018-easy-tuple-length(元组长度) 等同样围绕元组与数组展开的入门题。如果你想检验自己对元组类型操作的掌握程度,把它们放在一起练习是最高效的路径。
总结
First<T>虽然是一道 easy 题,但它覆盖了三个会在后续所有挑战中反复出现的核心知识点:
- 泛型约束:
T extends any[]限制了输入必须是数组,并让后续类型操作在编译期得到保障; - 元组模式匹配:
[infer F, ...any[]]的拆分能力是数组类类型编程的基石; - 边界处理:空数组返回
never,且必须与[undefined]这样的"有元素但元素是 undefined"场景严格区分。
把 template.ts 中那行type First<T extends any[]> = any替换为type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never,即可通过全部六条测试用例。理解这道题,你就已经掌握了类型级数组操作的第一块敲门砖。
- 示例工程
【免费下载链接】type-challenges
Collection of TypeScript type challenges with online judge
相关推荐
TypeScript 类型挑战 00014:实现 First\<T\> 获取数组第一个元素类型
TypeScript 类型挑战 00014:实现 First\<T\ 获取数组第一个元素类型 本篇文章围绕 type challenges 仓库中编号 0001
示例工程TypeScript 类型挑战 15:实现 `Last<T>` 泛型,提取数组最后一个元素类型
TypeScript 类型挑战 15:实现 Last<T 泛型,提取数组最后一个元素类型 导读 Last<T 是 type challenges 题库中的第 1
示例工程Llama2-Chinese-13b-Chat-ms模型融合原理:LoRA微调技术的实战应用
Llama2 Chinese 13b Chat ms模型融合原理:LoRA微调技术的实战应用 Llama2 Chinese 13b Chat ms是一款基于Ll
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考