携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第19天,点击查看活动详情
前言
本文记叙了使用 TypeScript 实现 join 类型工具的思路,与 javaScript 一样,这个类型工具的主要功能是将 数组 或者 元组 拼接成一个字符串。
实现
首先,这是类型工具,先不管它功能如何,形式如何,先把最基本的名称定义出来:
type Join = any
然后确定参数,一般接受两个参数,从 TypeScript 的角度来讲,就是接受两个泛型:
- 一个是分隔符
Delimiter,分隔符必然是string类型,且可以设置一个默认值,本文设置为,; - 一个是需要转换成为字符串的数组
List,List中每个元素必须可以转为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'
关键 List 的长度未定,不可能手写出所有的元素,而且 TypeScript 中也没有类似于 for 的工具类型,无法直接遍历 List。对于这个问题的解决方案,可以借助模式匹配 infer 和 递归进行解决:
- 每次获取
List的第一个元素(因为拼接成字符串时,也得按照数组的顺序),然后通过模式匹配获取剩余的元素infer Rest; - 对
Rest进行判断,如果不是空数组,则进行递归操作; - 借助模板字符串将每个元素拼接到字符串中:
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"
已经基本实现了拼接字符串,不过有一个小问题:
递归时,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;