TS-Join实现

522 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第19天,点击查看活动详情

前言

本文记叙了使用 TypeScript 实现 join 类型工具的思路,与 javaScript 一样,这个类型工具的主要功能是将 数组 或者 元组 拼接成一个字符串。

实现

首先,这是类型工具,先不管它功能如何,形式如何,先把最基本的名称定义出来:

type Join = any

然后确定参数,一般接受两个参数,从 TypeScript 的角度来讲,就是接受两个泛型:

  1. 一个是分隔符 Delimiter,分隔符必然是 string 类型,且可以设置一个默认值,本文设置为 ,;
  2. 一个是需要转换成为字符串的数组 ListList 中每个元素必须可以转为 string 才行,而 string | number | boolean | null | undefined 都可以被转换为 string ,所以 List 的类型为 Array<string | number | boolean | null | undefined>,可以把 List 中元素的联合类型提取出来,方便以后较对和修改:

综上,Join 加上泛型之后如下:

// 可以转换为 string 的类型
type CanConvertedToString = string | number | boolean | null | undefined

type Join<
  List extends Array<CanConvertedToString>, // 数组泛型
  Delimiter extends string = ',' // 分隔符泛型,默认为 `,`
> = any

接着,就开始真正实现功能了。

第一步,得想办法获取 List 中的元素,因为 List 是一个 Array ,所以自然可以通过索引 index 获取元素:

// 可以转换为 string 的类型
type CanConvertedToString = string | number | boolean | null | undefined

type Join<
  List extends Array<CanConvertedToString>, // 数组泛型
  Delimiter extends string = ',' // 分隔符泛型,默认为 `,`
> = List[0]

type JoinRes1 = Join<['abc', 123, boolean]> // JoinRes1: 'abc'

微信截图_20220820143119.png

关键 List 的长度未定,不可能手写出所有的元素,而且 TypeScript 中也没有类似于 for 的工具类型,无法直接遍历 List。对于这个问题的解决方案,可以借助模式匹配 infer递归进行解决:

  1. 每次获取 List 的第一个元素(因为拼接成字符串时,也得按照数组的顺序),然后通过模式匹配获取剩余的元素 infer Rest
  2. Rest 进行判断,如果不是空数组,则进行递归操作
  3. 借助模板字符串将每个元素拼接到字符串中:
type Join<
  List extends Array<CanConvertedToString>,
  Delimiter extends string = ','
> = List extends [CanConvertedToString, ...infer Rest] // 模式匹配,获取除第一个元素外的元素,注意,这里使用了扩展符,所以 infer 其实也是一个数组
  ? Rest extends [] // 判断剩余部分是否为空数组
  ? `${List[0]}` // 如果 Rest 是空数组,说明当前 List  只有一个元素,则返回 `List[0]`
  : `${List[0]}${Delimiter}${Join<Rest, Delimiter>}` // 如果 Rest 不是空数组,则开始拼接模板字符串,最后使用递归,继续取出 List 中每一个字符,直到 List 只剩一个字符为止
  : string; // 处理其他意外情况

type JoinRes1 = Join<['abc', 123, true]>; // "abc,123,true"

微信截图_20220820150725.png

已经基本实现了拼接字符串,不过有一个小问题:

微信截图_20220820150835.png

递归时,Rest 并没有被正确推导,反而被推导成 unknown[],所以递归传入 Rest 会有一个报错提示。解决也很简单,既然 TypeScript 推导不了,那就手动加上一层判断,对 Rest 进行收窄:

type Join<
  List extends Array<CanConvertedToString>,
  Delimiter extends string = ','
> = List extends [CanConvertedToString, ...infer Rest]
  ? Rest extends Array<CanConvertedToString> // 收窄,确定 Rest 就是 Array<CanConvertedToString>
  ? Rest extends []
  ? `${List[0]}`
  : `${List[0]}${Delimiter}${Join<Rest, Delimiter>}`
  : `${List[0]}` // 如果 Rest 不是 Array<CanConvertedToString>,则返回 List 第一个元素,不再递归,不过这个永远不会被执行
  : string;