刷几道 type challenges,入门 TypeScript 类型编程

版权声明:本人文章仅在掘金平台发布,请勿抄袭搬运,转载请注明作者及原文链接 🦉

阅读提示:网页版带有主题和代码高亮,阅读体验更佳 🍉

某日,我打开了 zustand (一个 React 轻量状态管理库) 的源码,看到了如下的 TypeScript 代码:

ts 复制代码
export type Mutate<S, Ms> = number extends Ms['length' & keyof Ms]
  ? S
  : Ms extends []
  ? S
  : Ms extends [[infer Mi, infer Ma], ...infer Mrs]
  ? Mutate<StoreMutators<S, Ma>[Mi & StoreMutatorIdentifier], Mrs>
  : never

好家伙,这都是些什么?这还是人看的代码吗?

我立马就关闭了 github,转手接了杯水,心里想这玩意有用么,这些人真卷。TypeScript 不就是类型标注么,还能这么玩?再说了,这破代码谁看得懂啊?我现在就只用 interface,万能,简单,实在不要太爽,最多不是多 copy 几遍,倒也没啥。

翌日,我又打开了 Vue3 的源码,不小心翻到了下面的代码:

ts 复制代码
export type EmitFn<
  Options = ObjectEmitsOptions,
  Event extends keyof Options = keyof Options,
  ReturnType extends void | Vue = void
> = Options extends Array<infer V>
  ? (event: V, ...args: any[]) => ReturnType
  : {} extends Options // if the emit is empty object (usually the default value for emit) should be converted to function
  ? (event: string, ...args: any[]) => ReturnType
  : UnionToIntersection<
      {
        [key in Event]: Options[key] extends (...args: infer Args) => any
          ? (event: key, ...args: Args) => ReturnType
          : (event: key, ...args: any[]) => ReturnType
      }[Event]
    >

我心态炸了,我就想看看源码,为啥这么多类型定义啊,想跳又不想走马观花,但是这类型代码是真难读懂啊,咋整?搜一下?

经过我一番搜罗,还真被我搜到了,首先找到了一本电子书:TS 类型挑战通关手册,主要是各种 TypeScript 类型编程题目的答案。那题目又是哪儿来的呢?又被我找到了,这本电子书是 vscode 插件 type-challenges 里题目的解析。type-challenges 又是啥?我赶紧打开 vscode 下载下来看看:原来是类似 leetcodeTypeScript 类型编程的插件,还带答案。里面的题目五花八门,还贴心的分了难度级别。

怎么办?搞不搞?思索再三,我的决定是:搞!因为我看那些复制来粘贴去的 interface 早就不顺眼了。

最主要是还可以用来装(电报声~)。

所以阅读本篇文章,要求你有一定的 TypeScript 基础,如果没有,掘金有几篇非常不错的入门文章:

一文搞定TypeScript所有类型,妈妈再也不怕我不会写TypeScript了

Ts高手篇:22个示例深入讲解Ts最晦涩难懂的高级类型工具

Typescript 类型编程,从入门到念头通达😇

TS 内置泛型工具函数

知己知彼,百战不殆。我们不妨先来看看 TypeScript 内置的类型定义文件:lib.es5.d.ts

该文件一方面定义了 es5 的所有 API 的类型:

ts 复制代码
/**
 * Evaluates JavaScript code and executes it.
 * @param x A String value that contains valid JavaScript code.
 */
declare function eval(x: string): any;

/**
 * Converts a string to an integer.
 * @param string A string to convert into a number.
 * @param radix A value between 2 and 36 that specifies the base of the number in `string`.
 * If this argument is not supplied, strings with a prefix of '0x' are considered hexadecimal.
 * All other strings are considered decimal.
 */
declare function parseInt(string: string, radix?: number): number;

...

但这些定义不是今天的主题,最主要的是来看看它内置的几个泛型工具函数。掌握这些泛型工具函数可以更好地帮助我们进行类型运算,如果你在此之前对泛型的概念都是模糊不清的,那么学习这些工具函数将有助于你更好地理解泛型。

现在,让我们来复习下,查缺补漏:

泛型工具 作用
Partial 泛型工具 Partial<T> 接受一个参数 TT键值对类型,将 T 所有属性设置为可选
Required 泛型工具 Required<T> 接受一个参数 TT键值对类型,将 T 所有属性设置为必填
Readonly 泛型工具 Readonly<T> 接受一个参数 TT键值对类型,将 T 所有属性设置为只读
Pick 泛型工具 Pick<T, K extends keyof T> 接受参数 TKT键值对类型KTkey 名,是联合类型。该函数在 T 中挑选出部分想要的 key 并返回新的键值对类型
Record 泛型工具 Record<K extends keyof any, T> 接受参数 KT。意思就是给某个 键值对类型 增加一个 key,并定义该 key 的类型。这里的 Kkey 名,T 是类型,也就是 type
Exclude 泛型工具 Exclude<T, U> 接受参数 TUExclude 中文意为排除,TU 都代表很多种类型,可以理解为获取 T U 的非交集部分
Extract 泛型工具 Extract<T, U> 接受参数 TUExtract 中文意为选段,和 Exclude 作用相反,取 T U 的交集部分
Omit 泛型工具 Omit<T, K extends keyof any> 接受参数 TKT键值对类型KTkey 名,是联合类型。该函数在 T 中排除部分 key(K) 并返回新的键值对类型
NonNullable 泛型工具 NonNullable<T> 排除 T 中的 null & undefined
ReturnType 泛型工具 ReturnType<T> 表示函数的返回类型
InstanceType 泛型工具 InstanceType<T> 获取构造函数类型的返回类型
Uppercase 泛型工具 Uppercase<S> 将字符串文字类型转换为大写
Lowercase 泛型工具 Lowercase<S> 将字符串文字类型转换为小写
Capitalize 泛型工具 Capitalize<S> 将字符串文字类型首字母转换为大写
Uncapitalize 泛型工具 Uncapitalize<S> 将字符串文字类型首字母转换为小写

上文中的所谓键值对类型,是指形如以下结构的类型,他们以键值对形式定义,一般是为了定义对象类数据。

ts 复制代码
interface Test {
  a: 1;
  b: 2;
}

type Test = {
  a: 1;
  b: 2;
}

上面的表格看完想必你也知道了一些你以前不知道的泛型工具,我以前就没接触过类似 UppercaseLowercaseCapitalizeUncapitalize 之类的泛型工具。这次我们就来看看这些泛型工具怎么使用。

必须掌握的泛型运算方法和特性

想要 TypeScript 进阶,光知道几个泛型工具还不够,还要能看懂各种泛型运算的代码。那么必不可少的就是需要掌握下列几个运算方法和特性。

keyof

所谓 keyof,看到 key 也大概能猜出来这个关键字是用在键值对类型上。其主要的作用就在于将一个键值对类型的所有的 key 获取到并转换为联合类型。例如:

ts 复制代码
type Test = {
  a: 1;
  b: 2;
}

type KeyOfTest = keyof Test; // 'a' | 'b'

const test1: KeyOfTest = 'a';
const test2: KeyOfTest = 'b';
const test3: KeyOfTest = 'c'; // Error 不能将类型""c""分配给类型"keyof Test"。

做到这里,其实我们也可以来举一反三,试下如果将关键字用在元组类型上会不会报错:

ts 复制代码
type MyTuple = [{}, 2, 3, undefined, null, '4', () => {}];

type KeyofMyTuple = keyof MyTuple;

const test4: KeyofMyTuple = 1;
const test5: KeyofMyTuple = '1';
const test6: KeyofMyTuple = null; // Error 不能将类型"null"分配给类型"keyof Tuple"。
const test7: KeyofMyTuple = undefined; // Error 不能将类型"undefined"分配给类型"keyof Tuple"。
const test8: KeyofMyTuple = {}; // Error 不能将类型"{}"分配给类型"keyof Tuple"。
const test9: KeyofMyTuple = []; // Error 不能将类型"[]"分配给类型"keyof Tuple"。

我们发现并不会报错,这就很神奇了,看来元组类型也能被 keyof(只是不报错,按理说是不能的),更有意思的是,当我们去给类型为 KeyofMyTuple 的变量赋值时,仅能赋值字符、数字类型,其他类型均会报错。虽然这个知识点用处不大,但是举一反三的思路还是有助于学习的,mark 住。

T[K]

JavaScript 中我们可以使用索引访问数组,key 名访问对象属性。在代码的编写形式上都表现为 T[K] 形式。TypeScript 中是否能以相同的方法访问元组类型与键值对类型的成员呢?

ts 复制代码
type A = 1;
type B = 2;
type C = string;
type MyTuple = [A, B, C];

type Test1 = MyTuple[0]; // type Test1 = 1
type Test2 = MyTuple[1]; // type Test2 = 2
type Test3 = MyTuple[2]; // type Test3 = string

怎么样,是不是看出规律了?索引访问元组类型也能获得其成员的类型,那我们再加加料,将索引替换为联合类型呢?

ts 复制代码
type A = 1;
type B = 2;
type C = string;
type MyTuple = [A, B, C];

type Test1 = MyTuple[0 | 1]; // type Test1 = 1 | 2
type Test2 = MyTuple[1 | 2]; // type Test2 = string | 2
type Test3 = MyTuple[0 | 1 | 2]; // type Test3 = string | 1 | 2

是不是又发现了新大陆,还能这么写?

看到这里,你应该彻底地认识到 TypeScript 是类型编程,它的语法特性不是能够常规照搬 JavaScript 就能简单理解的。

那我们来看看键值对类型通过 T[K] 形式访问成员是怎样的。

ts 复制代码
type Test = {
  a: 1,
  b: 2,
  c: '3',
  d: null
}

type A = Test['a']; // type A = 1
type B = Test['b']; // type B = 2
type C = Test['c']; // type C = "3"
type D = Test['d']; // type D = null

这很好理解,结果与预想中的一致。不过,我们前面说 keyof 会将键值对类型所有 key 重新组合成联合类型,要不在这里也试试?

ts 复制代码
type E = Test[keyof Test]; // type E = 1 | 2 | "3" | null

果然如此,这也符合我们的预料。

到这里,是不是对这两种关键字的作用都有一定认识了呢?

in 与对象属性遍历

首先,我们可以这么定义键值对类型的成员:

ts 复制代码
type TestObj = {
  ['a']: string;
  ['b']: number;
}

这种书写形式配合 in 方法能够实现遍历生成一个新的键值对类型:

ts 复制代码
type Test = {
  a: 1;
  b: 2;
  c: '3';
  d: null;
}

/**
  type Test1 = {
    a: 1;
    b: 2;
    c: "3";
    d: null;
  }
 */
type Test1 = {
  [P in 'a' | 'b' | 'c' | 'd']: Test[P]
}

这里相当于将 Test 的成员都遍历一遍,但是在实际场景下,我们需要遍历的类型成员是不定的,这里就需要引入泛型,传入一个不知道成员的键值对类型,并对其进行遍历。

来看书写形式:

ts 复制代码
type Test = {
  a: 1;
  b: 2;
  c: '3';
  d: null;
}

type TestIn<T> = {
  [P in keyof T]: T[P]
}

/**
  type Test1 = {
    a: 1;
    b: 2;
    c: '3';
    d: null;
  }
 */
type Test1 = TestIn<Test>;

这里就运用上了之前我们讲的 keyof 关键字。

上述就是一个键值对类型遍历的固定写法,学会键值对类型遍历可以做很多事情,例如,我们需要将其成员都变成只读。

元组特性

上面其实已经提到了一些元组的特性,例如索引访问。

但是元组还有一些特性。例如,JavaScript 里的数组有 length 属性,那元组是否也有?不卖关子,直接上答案:

MyTuple['length'] 能够获取到元组的长度。

ts 复制代码
type MyTuple = [1, 2, string];
type Test = MyTuple['length']; // type Test = 3

MyTuple[number] 能够获取到所有元组成员组成的联合类型。

ts 复制代码
type MyTuple = [1, 2, string];
type Test = MyTuple[number]; // type Test = string | 1 | 2

[...MyTuple] 元组也具备解构的特性。

ts 复制代码
type MyTuple = [1, 2, string];

type Test = [...MyTuple]; // type Test = [1, 2, string]

extends

extends 有两层含义。

一方面在 interface 和类上,表示继承扩展。例如:

ts 复制代码
interface Test {
  a: string;
  b: string;
}

interface Test1 extends Test {
  c: number
}

// 不设置成员 a b c 会报错
class Obj implements Test1 {
  a = '1'
  b = '2'
  c = 3
}

class Obj1 extends Obj {
  d = 2;
  say() {
    console.log('c', this.c)
    console.log('d', this.d)
  }
}

const obj1Ins = new Obj1();
obj1Ins.say(); // c = 3,d = 2

这里使用了一个关键字 implements:实现。

所谓实现就是定义一个接口,并用一个类实现这个接口,一般来说,我们的 interface 应该只给类定义类型,普通对象的类型应该使用 type 来定义。不过现在大多数人都使用 interface 来定义普通对象的类型,不是不可以,只是不太规范罢了。

extends 另一层含义是指前一个类型是否是后一个类型的子类型。

ts 复制代码
type A = 1 | 2;

type B = 1;

type C = A extends B ? A : B; // type C = 1

也就说,A 不是 B 的子类型。这也很好理解,毕竟 A 的范围比 B 的范围更大。那相反的,B 就是 A 的子类型。

ts 复制代码
type A = 'a' extends 'a' ? 1 : 2; // type A = 1

自身属于自身的子类型。

知道这些有什么用呢?那当然有大用,特别是判断类型时有大用。

三目条件语句

三目运算符想必都不陌生了。

TypeScript 泛型运算中没有 if else,基本靠三目表达式完成条件判断,TypeScript 中的三目可以书写很多次,进行多层嵌套。

三目表达式离不开一个关键字,那就是 extends。上面不是说了 extends 能够起到判断类型父子关系的作用吗?结合三目表达式就能达到非常厉害的条件类型。那么可以说,extends 的使用除了继承扩展外,另一处的使用基本就是配合三目表达式使用了。

ts 复制代码
type Test<T> = T extends 'a' 
  ? 'a'
  : T extends 'b'
    ? 'b'
    : T extends 'c'
      ? 'c'
      : T extends 'd'
        ? 'd'
        : 'e';

never

never 表示的是那些永不存在的值的类型。其实就是作为条件判断的出口。就比如:

ts 复制代码
type Test<T> = T extends 'a' 
  ? 'a'
  : T extends 'b'
    ? 'b'
    : never

在上面类型中,我明确的知道泛型 T 可能是 'a''b' 中的某一个,它不可能再是其它某一个类型,你说可以用 anyunknown 啊,这并不可行,因为那也就意味着 T 还有可能是其它类型,到时候你给 T 赋值其它类型,TypeScript 不会报错,失去了意义。

这就是 never 的作用,特别是在三目表达式中非常有用,可以作为某些条件的跳出类型。

关于 never,可以在后续的题目中体会它的妙用。

infer

infer 表示推断类型。

更重要的是:仅条件类型的 "extends" 子句中才允许 "infer" 声明

这句话的意思就是 infer 只能在条件语句中使用,条件语句目前就是三目表达式且必有 extends 关键字,infer 只能在 extends 后使用。

例如:

ts 复制代码
type Test<T> = T extends infer R ? 1 : 2 // 已声明"R",但从未读取其值

infer 有啥用?

举个例子,我现在有一个自定义的泛型工具,传入一个元组类型,给我识别出元组最后一个成员的类型。这怎么做?元组作为参数,你是不知道它的成员及长度的,但是却需要取出它最后一位成员的类型,是不是有点难度?结合 infer 其实很简单。

ts 复制代码
type MyTuple = [1, 2, string];

type GetTupleLastOne<T> = T extends [...infer Rest, infer L] ? L : never;

type LastOne = GetTupleLastOne<MyTuple>; // type LastOne = string

这里的案例基本已经进入我们今天文章的主题了,是不是已经感受到难度了(如果你是新手)?

这里基本把我们之前的知识点串起来了,首先是三目表达式,其次是 extends,然后是元组的解构特性,最后是 never 作为逃出类型。

模板字符串类型

假设现在我有一个博客网站:https://blog.com。博客下有很多文章,文章都是一些 id。每次访问文章都是类似的地址:

  • https://blog.com/1
  • https://blog.com/2
  • https://blog.com/3

这种数据的类型如何定义?

我们就假设现在文章就只有 5 篇,我们需要给文章链接定义类型,难道要这样定义吗?

ts 复制代码
type ArticleUrl = 
| 'https://blog.com/1'
| 'https://blog.com/2'
| 'https://blog.com/3'
| 'https://blog.com/4'
| 'https://blog.com/5'

除了主路由外就只是文章 id 不一样罢了。JavaScript 里有模板字符串,TypeScript 里也有。

上面的类型可以这么定义:

ts 复制代码
type ArticleId = '1' | '2' | '3' | '4' | '5';
type ArticleUrl = `https://blog.com/${ArticleId}`;

JavaScript 中的模板字符串是在运行时确定变量的值,TypeScript 是在编译时确定变量的类型。

了解模板字符串类型可以帮助我们更好地运用字符串类型。

例题实战

前面将一些知识给大家做一遍铺垫,利于大家更好地学习类型编程。但是还有一些特性一句话两句话讲不清楚,没有案例是很难理解的,所以我们放到题目中去理解,效果更佳。

JavaScript 中我们经常有手写 callapply,实现一个数组 map 等手写实现原生方法的面试题,这不仅考察我们的语法,也考察我们的编程能力。

TypeScript 也一样,想要快速进阶的方法也是实现一遍原生 API,这更有助于我们理解原生 API 的参数和特性。

MyPartial<T>

描述:实现一个 MyPartial<T>,功能和原生 Partial 一致。

分析:Partial 接受一个键值对类型,并将其每一个属性成员都变成可选。关键信息:键值对类型,可选,所有成员。那么必须用到属性遍历,这是肯定的。回忆下属性遍历写法:

ts 复制代码
type Test<T> = {
  [P in keyof T]: T[P]
}

属性可选只需要加上 ? 即可。那么答案不言自明:

ts 复制代码
type Test<T> = {
  [P in keyof T]?: T[P]
}

MyRequired<T>

描述:实现一个 MyRequired<T>,功能和原生 Required 一致。

分析:Required 接受一个键值对类型,并将其每一个属性成员都变成必填。方法同上,但是怎么必填法?这里有固定写法:可选属性前加 -

ts 复制代码
type Test<T> = {
  [P in keyof T]-?: T[P]
}

看下测试结果:

ts 复制代码
type Test = {
  a?: 1;
  b?: 2;
}
type MyRequired<T> = {
  [P in keyof T]-?: T[P]
}

/**
 type Test1 = {
    a: 1;
    b: 2;
  }
 */
type Test1 = MyRequired<Test>; 

MyReadonly<T>

描述:实现一个 MyReadonly<T>,功能和原生 Readonly 一致。

答案:

ts 复制代码
type MyReadonly<T> = {
  Readonly [P in keyof T]: T[P]
}

MyPick<T, K>

描述:实现一个 MyPick<T, K>,功能和原生 Pick 一致,且不使用任何原生泛型工具。

分析:不管三七二十一,键值对类型我先遍历写上。

注意,Pick 第二个参数是联合类型,且是第一个类型中的部分 key

ts 复制代码
type MyPick<T, K> = {
  [P in keyof T]: T[P]
}

K 既然是 T 中的 key 组成的联合类型,需要限制一下:

ts 复制代码
type MyPick<T, K extends keyof T> = {
  [P in keyof T]: T[P]
}

这里有个问题,就是我们的常规遍历是将类型上所有 key 遍历一遍,但是 K 又限制了我们取值的范围,题目中还要求不能使用原生泛型工具,类似 OmitExclude 等方法也不能被使用,大大限制了我们的发挥。只能寻求他法。

这里也不卖关子,需要使用到类型断言 as。最终答案如下:

ts 复制代码
type MyPick<T, K extends keyof T> = {
  [P in keyof T as P extends K ? P : never]: T[P]
}

利用 as 再进行一遍类型范围的缩小,将 P 限制在 K 的范围内,若在 K 范围内则返回 P,否则返回 never,这里的 never 就是作为一种逃出条件,我并不想返回任何类型,但是又必须满足三目表达式,因此用 never 逃出条件。

MyOmit<T, K>

描述:实现一个 MyOmit<T, K>,功能和原生 Omit 一致,且不使用任何原生泛型工具。

分析:知道了 MyPick 怎么实现的,Omit 不是信手拈来吗?

答案:

ts 复制代码
type MyOmit<T, K extends keyof T> = {
  [P in keyof T as P extends K ? never : P]: T[P]
}

GetOptional<T>

描述:实现一个 GetOptional<T>,功能是将键值对类型中的可选属性都挑选出来组合成新的键值对类型。

例如:

ts 复制代码
GetOptional<{ foo: number; bar?: string }>; // { bar?: string }

分析:在前面我们基本知道了键值对类型的遍历,以及利用 as 缩小 key 的范围。这题还是键值对类型,依旧是和可选属性打交道,仍然不管如何,先把遍历写上。

ts 复制代码
type GetOptional<T> = {
  [P in keyof T]: T[P]
}

问题在于将可选属性挑选出来,之前我们是有第二个参数 K,通过 as P extends K 来限制 key 的范围。这次没有参数 K,如何限制?

这里其实还有另一种写法:as T[P] extends Required<T>[P]

假设这里传入的 T 等于 { foo: number; bar?: string }

Required<T> 等于 { foo: number; bar: string }

T[P] 等于 T[foo]T[bar?]Required<T>[P] 等于 Required<T>[foo]Required<T>[bar]

T[bar?]Required<T>[bar] 区别就在于这个 ?bar? 也就被排除出来了。

答案:

ts 复制代码
type GetOptional<T> = {
  [P in keyof T as T[P] extends Required<T>[P] ? never : P]: T[P]
}

GetRequired<T>

描述:实现一个 GetRequired<T>,功能是将键值对类型中的必填属性都挑选出来组合成新的键值对类型。

分析:GetOptional<T> 都会了,GetRequired<T> 不是信手拈来吗?

答案:

ts 复制代码
type GetRequired<T> = {
  [P in keyof T as T[P] extends Required<T>[P] ? P : never]: T[P]
}

KeysToUnion<T>

描述:实现一个 KeysToUnion<T>,功能是将键值对类型中的属性转为联合类型。

分析:直接上答案。

答案:

ts 复制代码
type KeysToUnion<T> = {
  [P in keyof T]: P
}[keyof T];

ValuesToUnion<T>

描述:实现一个 ValuesToUnion<T>,功能是将键值对类型中的 value 类型转为联合类型。

分析:直接上答案。

答案:

ts 复制代码
type ValuesToUnion<T> = {
  [P in keyof T]: T[P]
}[keyof T];

TupleFirst<T>

描述:实现一个 TupleFirst<T>,功能是获取元组的第一个类型。

分析:前面我们讲过如何获取元组最后一位的类型。

ts 复制代码
type TupleLast<T extends any[]> = T extends [...infer Rest, infer L] ? L : never;

那么这题也类似,只需要颠倒下位置即可。

ts 复制代码
type TupleFirst<T extends any[]> = T extends [infer F, ...infer Rest] ? F : never;

但是,我们还知道元组是可以进行索引访问的,第一个不就是 T[0] 吗?但是传过来的元组可能是空的,直接 T[0] 会报错,所以需要对空元组做下判断。

那么我们就有了答案:

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

总结下答案:

ts 复制代码
type TupleFirst<T extends any[]> = T extends [infer F, ...infer Rest] ? F : never;

type TupleFirst<T extends any[]> = T['length'] extends 0 ? never : T[0];

TupleToUnion<T>

描述:实现一个 TupleToUnion<T>,功能是将元组转换为联合类型。

分析:讲元组的时候我们写过。

ts 复制代码
type TupleToUnion<T extends any[]> = T[number];

TupleToObject<T>

描述:实现一个 TupleToObject<T>,功能是将元组转换为键值对类型。

转换前:

ts 复制代码
const tuple = ['tesla', 'model 3', 'model X', 'model Y']

转换后:

ts 复制代码
{
  'tesla': 'tesla';
  'model 3': 'model 3';
  'model X': 'model X';
  'model Y': 'model Y'
}

分析:还是遍历,关键是 key 的范围要限制为元组的成员。

答案:

ts 复制代码
type TupleToObject<T extends readonly any[]> = {
  [P in T[number]]: P
}

Reverse<T>

描述:实现一个 Reverse<T>,功能是将元组的成员进行翻转,类似于 JavaScript 中的 reverse

分析:TypeScript 可没有提供类似的泛型工具,不过,我们前面知道,可以通过 infer 提取元组的成员。但是只能提取第一个或者最后一个。不过,我要是说 TypeScript 类型也能递归,你是不是有点想法了?

ts 复制代码
type Reverse<T extends any[]> = T extends [infer F, ...infer Rest]
?
: []

关键在于这个 ? 后的表达式该怎么写,提取第一个元素后,怎么办呢?是不是得放在最后面?那不就是 [...infer Rest, infer F]

但是这不就是换了下顺序嘛,怎么递归法?

这样?

ts 复制代码
type Reverse<T extends any[]> = T extends [infer F, ...infer Rest] 
? Reverse<[...infer Rest, infer F]>
: []

这样好像会无限循环诶,而且第一个最后换下去不就又回到第一个了吗?

其实答案已经呼之欲出了:

ts 复制代码
type Reverse<T extends any[]> = T extends [infer F, ...infer Rest]
? [...Reverse<Rest>, F]
: []

这样是不是就不会死循环了?

Push<T>

描述:实现一个 Push<T>,功能是往元组末尾添加元素,类似于 JavaScript 中的 push

分析:可还记得元组也具备解构的特性?

ts 复制代码
type Push<T extends any[], U> = [...T, U]

那类似 popshiftunshift 等功能岂不是信手拈来?

TrimLeft<T>

描述:实现一个 TrimLeft<T>,功能是取出字符串类型的左侧空格。

分析:字符串类型的处理,目前我们还没接触过,只知道一个模板字符串类型。字符串里的空格可能是空格,也可能是换行符,可能是制表符,所以需要把换行符和制表符考虑进来。

ts 复制代码
type Space = ' ' | '\n' |'\t';
type TrimLeft<S extends string> = S extends `${Space}${infer R}` ? R : S;

对于空符而言,它不可能只是一个,可能是多个,所以上面的答案还有点缺陷,需要递归处理下。

ts 复制代码
type Space = ' ' | '\n' |'\t';
type TrimLeft<S extends string> = S extends `${Space}${infer R}` ? TrimLeft<R> : S;

Trim<T>

描述:实现一个 Trim<T>,功能是取出字符串类型的左右两侧空格。

分析:之前左侧的会写了,那无非就是右侧也加一下 Space 而已。

你可能会这么写:

ts 复制代码
type Space = ' ' | '\n' |'\t';
type Trim<S extends string> = S extends `${Space}${infer R}${Space}`
? Trim<R>
: S;

但是很残酷,这样并行不通,因为 ${Space}${infer R}${Space} 的意思是说左边有空符,右边也有空符。那我要是只有左边有,或者只有右边有呢?甚至于说两边都没有。

所以得分开来写,当左边有或者当右边有时。

ts 复制代码
type Space = ' ' | '\n' |'\t';
type Trim<S extends string> =
  S extends `${Space}${infer R}` | `${infer R}${Space}`
    ? Trim<R>
    : S;

这里其实涉及到一个知识点没有讲:联合类型的分发性。

T extends U 此类表达式右侧的 U 是联合类型时,会触发联合类型的分发性。所谓分发,举例来说:

ts 复制代码
type Union = 1 | 2 | 3;

type TestNumber<N> = N extends Union ? N : never;

这里的 N extends Union ? 实际上是:

ts 复制代码
N extends 1 ?
N extends 2 ?
N extends 3 ?

其实就是一个一个去比较联合类型的成员。

写在最后

基本上今天的题目就讲这么多,更多的题目大家可自行去挑战,答案也都有。

多刷几道题就能体会到类型编程的魅力,不过大家不要走火入魔,平时工作中的类型还是尽量简单些好,复杂类型只会让项目维护变得越来越困难。

另外,对于需要写库和阅读开源代码的同学来说,学会类型编程就是能力提升的一大路径,不仅看开源代码更加容易了,自己在写库时,也能写出优雅的类型。

往期推荐

别人休息我努力,悄悄写个 cli 工具,必须提升效率,skr~ 60+ 👍🏻 110+ 💚

一文掌握 eslint,再也不怕项目报错 20+ 👍🏻 30+ 💚

开发一个 npm 库应该做哪些工程配置? 40+ 👍🏻 50+ 💚

分享我在前端学习与开发中用到的神仙网站和工具 40+ 👍🏻 110+ 💚

uniapp 踩坑记录(二) 130+ 👍🏻 150+ 💚

闲来无事,摸鱼时让 chatgpt 帮忙,写了一个 console 样式增强库并发布 npm 100+ 👍🏻 110+ 💚

uniapp 初体验踩坑记录 30+ 👍🏻 60+ 💚

两小时学会 JS 正则表达式,终身不忘 50+ 👍🏻

【一年前端必知必会】如何写出简洁清晰的代码 50+ 👍🏻

【一年前端必知必会】了解 Blob,ArrayBuffer,Base64 40+ 👍🏻 90+ 💚

相关推荐
刺客-Andy10 小时前
React 第十九节 useLayoutEffect 用途使用技巧注意事项详解
前端·javascript·react.js·typescript·前端框架
哥谭居民000121 小时前
将一个组件的propName属性与父组件中的variable变量进行双向绑定的vue3(组件传值)
javascript·vue.js·typescript·npm·node.js·css3
一條狗1 天前
隨筆20241226 ExcdlJs 將數據寫入excel
react.js·typescript·electron
冰红茶-Tea1 天前
typescript数据类型(二)
前端·typescript
TSFullStack1 天前
TypeScript 数据类型 - 数组
typescript
Web阿成3 天前
3.学习webpack配置 尝试打包ts文件
前端·学习·webpack·typescript
j喬乔3 天前
Node导入不了命名函数?记一次Bug的探索
typescript·node.js
yg_小小程序员4 天前
vue3中使用vuedraggable实现拖拽
typescript·vue
高山我梦口香糖4 天前
[react 3种方法] 获取ant组件ref用ts如何定义?
typescript·react
prall4 天前
实战小技巧:下划线转驼峰篇
前端·typescript