ts类型体操之 AllCombinations(中等)

由题意可知我们的目标是将一个字符串进行拆解,然后形成不限制数量的各种 自由组合

很明显我们需要一个能够将字符串中的每个字符单独拆分,并最终将所有的拆分结果形成一个联合类型的工具函数:

type StringToUnion<S> = S extends `${infer F}${infer R}` ? F | StringToUnion<R> : ''

利用 infer F 来获取字符串的首个字符,然后递归字符串的剩余部分 infer R,最终通过 将后续递归的结果形成联合类型。

看看效果:

type Test = StringToUnion<'ABC'>
// Test = '' | 'A' | 'B' | 'C'

此时已经对字符串进行了初步的拆解,这就相当于搭积木有了基本的积木块,但是还需要去组合这些积木块才能达成目标。

抛开整个问题全貌,先思考这样一个问题:各种组合是如何拼接出来的?

下面举例 BAC 的组合过程:

首先,需要在原始的字符串 ABC 中将 B 去掉形成 AC,然后拿出拆解出来的字符 B 拼接在 AC 的前面最终形成

组合 BAC,其余的组合也都是如此实现,但是如何实现呢?

先别急,在正式解决这个问题之前,先来实现一个从字符串中去掉指定字符的工具函数:

type ExcludeString<S, T extends string> = S extends `${infer A}${T}${infer B}` ? `${A}${B}` : S

通过 infer ATinfer B 来匹配目标字符串 T,匹配成功就返回除 T 之外的内容,否则直接返回原字符串

看看效果:

type Test = ExcludeString<'ABC', 'B'>
// Test = 'AC'

太棒了,这和上面思考过程的效果一样!

回到题目,此时已经有了工具函数可以使用,我们可以先将原始字符串 S 的每一个字符转换为联合类型 T 从而获取最基本的 素材

type AllCombinations<S extends string, T extends string = StringToUnion<S>> = any

这时你一定想起了在前面的类型体操 029 medium permutation 中,热门答案详细解释了如何通过 T extends T的方式来在条件语句中 展开遍历 一个联合类型 传送门

是的,此时就需要利用这个特性来遍历联合类型 T,从而实现对展开后的联合类型 T 中每个类型的处理

此时代码变成:

type AllCombinations<S extends string, T extends string =
  StringToUnion<S>> = T extends T ? /*do smth*/  : S

type Result = AllCombinations<'ABC'>

// T = '' | 'A' | 'B' | 'C' 注意此处展示的是 T 的类型,Result 后面再说

上面我们提到了组合的形成过程,先来尝试从原字符串中去掉一个字符看看效果,改变 do smth 部分:

type AllCombinations<S extends string, T extends string =
  StringToUnion<S>> = T extends T ? `${ExcludeString<S, T>}`  : S

type Result = AllCombinations<'ABC'>

// Result = 'ABC' | 'BC' | 'AB' | 'AC'

因为 T = '' | 'A' | 'B' | 'C',将联合类型的每个分支分别代入 ExcludeString 就不难理解为什么结果是 'ABC' | 'BC' | 'AB' | 'AC'

相信你此时一定灵光一闪:将结果中的 'ABC' | 'BC' | 'AB' | 'AC' 再次作为参数传入 AllCombinations 进行递归,不就能得到最后的结果了吗?

来尝试一下,将代码修改为:

type AllCombinations<S extends string, T extends string =
  StringToUnion<S>> = T extends T ? `${AllCombinations<ExcludeString<S, T>>}`  : S

type Result = AllCombinations<'ABC'>

// ERROR: 类型实例化过深,且可能无限

哈?why?

别慌,我们来思考下无限递归是如何出现的

先看联合类型 T 的第一个分支,即 T 为 ”,此时代码执行情况为:

// 第一次执行
AllCombinations<ExcludeString<'ABC', ''>>

ExcludeString<'ABC', ''> 的执行结果为 ABC

接下来,ABC 作为参数传入下一次递归中:

// 递归
AllCombinations<ExcludeString<'ABC', ''>>

原来如此,当 T 的类型为 ” 时,代码进入了无限递归。现在尝试来排除这种情况:

type AllCombinations<S extends string, T extends string =
  StringToUnion<S>> = T extends T ? `${T extends '' ? T : AllCombinations<ExcludeString<S, T>>}`  : S

type Result = AllCombinations<'ABC'>

// Result = "" | "ABC" | "A" | "BC" | "B" | "C" | "AB" | "CB" | "AC" | "ACB" | "CA" | "BA" | "BAC" | "BCA" | "CAB" | "CBA"

此时题目中的测试用例已经全部通过,目标达成,此刻代码全貌:

type StringToUnion<S> = S extends `${infer F}${infer R}` ? F | StringToUnion<R> : ''

type ExcludeString<S, T extends string> = S extends `${infer A}${T}${infer B}` ? `${A}${B}` : S

type AllCombinations<S extends string, T extends string = StringToUnion<S>> = T extends T ? `${T}${T extends '' ? T : AllCombinations<ExcludeString<S, T>>}` : S

当然,这种方式并不是最巧妙的方式,但希望通过展开整个思考过程的方式能够帮助到有需要的人