TypeScript 类型体操之 First of Array

TypeScript 类型体操之 First of Array

题目要求

实现一个通用 First<T>,它接受一个数组 T 并返回它的第一个元素的类型。

例如:

ts 复制代码
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

题目实现

找到数组 T 第一项的类型还是比较简单,只需要 T[0] 即可,但是问题就是,如果数组为空,数组第一项的类型就是 never,所以题目的难点变成了使用 TS 判断一个数组是否为空数组。

看了一遍评论区之后,找到了下面几种解决方案:

1、T['length'] extends 0

ts 复制代码
type First<T extends any[]> = T['length'] extends 0 ? never : T[0]

T extends any[] 表示类型约束,用来限制 T 为一个数组。

而 T['length'] extends 0 中的 extends 来表示条件类型,在 TS 中 A extends B ? C : D 的意思是"如果 A 可以赋值给 B,那么类型就是 C,否则就是 D"。

在此表达式中 "B" 为 0 也就是说如果条件成立,那么 T['length'] 只能为 0,到此我们就完成了数组是否为空的判断。

综上,我们可以实现了 First<T> 返回 T 的第一个元素的类型。

2、T extends []

ts 复制代码
type First<T extends any[]> = T extends [] ? never : T[0]

和第一种实现方式类型,不过使用了 T extends [] 来判断了 T 类型是否为空数组。

3、T[number] extends never

ts 复制代码
type First<T extends any[]> = T[number] extends never ? never : T[0];

T 是一个数组,T[number] 表示 T 的下标为 number 的元素类型,也就是 T 中所有可能的元素类型组成的联合类型。

T[number] extends never 表示如果 T 的所有元素可能的类型是 never,也就是说 T 是一个空数组,则返回 never 类型;否则,返回 T 的第一个元素的类型。

4、'0' extends keyof T

ts 复制代码
type First<T extends any[]> = '0' extends keyof T ? T[0] : never;

我们可以通过 keyof T 来获取类型 T 所有键的类型组合,那么如果 '0' 是 T 的键就表示 T 是一个非空数组。

如果 T 是一个非空数组, 返回 T[0],否则,返回 never 类型。

5、infer

在 TS 中,我们通过 extends 和 infer 可以实现一个类似于模式匹配的效果。前面说过 A extends B ? C : D 的意思是"如果 A 可以赋值给 B,那么类型就是 C,否则就是 D"。而在 B 中我们把某个类型变量使用 infer T 来表示,并可以获取它。

举个例子:

ts 复制代码
// 我们使用 (...args: infer P) => any 表示了一个函数声明,其中使用 infer P 来表示函数的类型参数
// 如果传入的参数能够赋值给函数声明,就可以通过 P 来获取参数类型了
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

// 我们使用 T extends (...args: any) => infer R 表示了一个函数声明,其中使用 infer R 来表示函数的返回参数
// 如果传入的参数能够赋值给函数声明,就可以通过 R 来获取返回类型了
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

感觉有点类型于正则匹配,/<([^>]*)>/.exec('<html>') 我们使用 () 包裹要获取的内容,当整个正则和字符串匹配的时候,我们就能获取真正对应的字符串了。

现在我们可以实现题目

ts 复制代码
type First<T extends any[]> = T extends [infer A, ...infer _rest] ? A : never

在 T extends [infer A, ...infer _rest] ? A : never 中,如果 T 是一个至少有一个元素的数组,那么 A 就会推断为 T 的第一个元素的类型,然后返回 A;否则,返回 never 类型。

对 TypeScript 的理解

做完这道题突然有一个很大的收获,TypeScript是针对类型的编程,而不是针对数据的编程。

在做这道题的时候一直纠结一个问题

ts 复制代码
type First<T extends any[]> = T extends [infer A, ...infer rest] ? A : never

const arr = [1, 2, 3];
type ArrType = typeof arr
type ArrFirstType = First<ArrType>; // never

为什么在这种情况,arr 明明不是空数组,却会返回 never。但是后面突然明白了,当我通过 typeof arr 获取类型时,我们获取的是 number[],和 arr 具体的值并无关系。

TS 是在静态编译时对类型的限制,和运行时的具体数据并无关系。

我们对类型 [number, string] 获取第一个类型肯定为 number,对类型 [1, '2'] 获取第一个类型为 1,而对 [] 获取第一个类型则为 never。

但是对于 number[] 来说,它并不一定有第一个元素,所以我们也没办法获取第一个元素的类型。所以个人认为这是一个未定义的结果。而对于测试样例,我们也能看到测试的类型都是具体的类型。所以对于上面的不同解法,对 number[] 这种情况,返回 number 或 never 都在可理解范围。

ts 复制代码
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>>
];
相关推荐
西安小哥5 分钟前
端侧跑大模型,真正的门槛不在参数量:一次 596 秒到 10 秒的压缩路径复盘
前端·人工智能
陈随易27 分钟前
Cloudflare 出钱,Vite 团队挑人,44 个维护者分 30 万美元
前端·后端·程序员
恋猫de小郭1 小时前
Flutter THLC 的 WebView 滚动终于从 70 FPS 提升到 120 FPS
android·前端·flutter
严肃小白兔2 小时前
TanStack Router 更新构建工具链:Node.js 兼容性、路由分割与前端工程实践
前端·node.js·状态模式
IT_陈寒2 小时前
Vue这个奇葩的响应式问题让我加班到凌晨
前端·人工智能·后端
风骏时光牛马2 小时前
功能可用性自动化测试验证
前端
LucianaiB12 小时前
【元宝搜索开发者征文大赛】AI时代,学得慢就不用学?用元宝搜索打造一个教程保质期,确保每一个教程符合你的现状
前端·人工智能·ai编程
excel12 小时前
JavaScript 对象池:减少高频对象创建与 GC 压力
前端
陈随易14 小时前
纯 Go 写的 JS 引擎,跑分超过了 C 语言的 QuickJS
前端·后端·程序员
子兮曰16 小时前
Chrome 权限元素实战:144 与 151 的渐进增强写法
前端·javascript·chrome