news 2026/9/30 6:34:30

type-challenges 第 14 题:实现 Firstlt;Tgt; 泛型,提取元组首元素类型

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
type-challenges 第 14 题:实现 Firstlt;Tgt; 泛型,提取元组首元素类型
  • 示例工程

【免费下载链接】type-challenges

Collection of TypeScript type challenges with online judge

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

本篇指南基于 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

模板已经把两件事交代清楚了:

  1. 泛型参数必须被约束为数组:T extends any[]。这保证了传入First的一定是数组或元组类型,而不是字符串、对象等。
  2. 返回值处是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

项目地址:https://gitcode.com/GitHub_Trending/ty/type-challenges
点击查看免费下载
上一篇:【亲测免费】 探索游戏新边界:Apollo Save Tool(PS4)- 玩家的宝藏工具箱
下一篇:MAA自动公招怎么配置:三步搞定一键招募,加急许可刷光券

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

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

纯Verilog实现FPGA硬解PNG:DEFLATE与Huffman解码实战

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

作者头像 李华
网站建设 2026/9/30 6:32:53

VMware搭建Windows Server:DNS与IIS Web站点配置实战

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

作者头像 李华
网站建设 2026/9/30 6:32:38

Linux面试题大全:覆盖命令、权限、进程、网络与Shell脚本

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

作者头像 李华
网站建设 2026/9/30 6:30:39

Vue3中安全获取当前路由的四种方法与实战选型指南

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

作者头像 李华
网站建设 2026/9/30 6:30:12

面积法到消点法:几何定理机器证明的底层逻辑与解题实战

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

作者头像 李华