# 基操-类型推断的边界条件
在 TS 中,使用 infer 和 extends,基本就是走向了高级的用法,而在 TS 类型体操中,对于字符和元组的题目,借助推断,可以找到特定位置的元组,而辅以递归,就可以实现遍历。此时,边界条件就显得比较重要。
我本人在一刷题目的时候,有些题目,隐隐感觉边界没有处理,但是还是正常工作,有些边界特殊处理了,但是看别人的答案好像不需要处理。
这一切的一切,都会在本篇中讲解清楚,如果你也有这方面的困惑,那么这篇绝对能帮你少走特别多的弯路。
# 字符串
infer 本身可以用于字符串的推导,这在 ts 体操中非常常见,但是最常见的一个问题就是边界的问题,看下面的例子:
type Case<T extends string> = T extends `${infer F}${infer L}` ? L : 2;
type Res1 = Case<'a'>; // '',extends 走 true 逻辑,L 为空
type Res2 = Case<''>; // 2,凑不够一个字符,extends 走 false 逻辑
type Res3 = Case<'abdab'>; // 'bdab' , extends 走 true 逻辑,L 为 'bdab'
type Case2<T extends string> = T extends `${infer F}${infer M}${infer L}`
? L
: 2;
type Res21 = Case2<'a'>; // 2,凑不够两个字符,走 false 逻辑
type Res22 = Case2<''>; // 2,凑不够两个字符,走 false 逻辑
type Res23 = Case2<'ab'>; // '', extends 走 true 逻辑,L 为空
type Res24 = Case2<'abcd'>; // 'cd', extends 走 true 逻辑,L 为 cd
总结下来,根据 infer 推断的数量 n,如果字符数量少于 n - 1,那么就会走 false 逻辑,否则,走 true 逻辑,前面的 infer 都是一个字符,最后一个 infer 承接剩余的字符,承接的剩余字符可以接受为空字符。
特殊情况下,如果 infer 推断的数量只有 1 个,那么永远不会走 false 逻辑,且推断的始终是整个字符串。如下:
type Case3<T extends string> = T extends `${infer F}` ? F : 2;
type Res31 = Case3<'a'>; // 'a'
type Res32 = Case3<''>; // ''
type Res33 = Case3<'ab'>; // 'ab'
type Res34 = Case3<'abcd'>; // 'abcd'
除此之外,对于字符,还有一些场景,就是中间有固定元素隔开的推断,比如:
type Case4<T extends string> = T extends `${infer F}hahaha${infer R}`
? `${F}${R}`
: 2;
type Res41 = Case4<'hahaha'>; // ''
type Res42 = Case4<'ahahahab'>; // 'ab', F -> a, R -> b
type Res43 = Case4<'abchahahadef'>; // 'abcdef', F -> abc, R -> def
type Case5<T extends string> = T extends `${infer F}${infer M}hahaha${infer R}`
? M
: 2;
type Res51 = Case5<'hahaha'>; // 2, 前缀 F,M 的推断要求必须有一个字符
type Res52 = Case5<'ahahahab'>; // '', M -> '' F -> a, R -> b
type Res53 = Case5<'abchahahadef'>; // 'bc' M -> bc, F -> a, R -> def
可以看出来,对于中间有固定元素间隔的情况,那么就会划分成左右两边,此时各自的最后一个推断承接剩余字符,同时也会在满足上述说的字符数量不足的情况时走 false 的逻辑。关于这种情况的题目,可以看看 147-cPrintfParser 的内容。
# 元组
数组的推断边界同理,只是数组可以使用扩展操作符 ...
从而改变获取剩余元素的位置,同时由于 ...
操作符的存在,不带 ...
操作符,则表示一个元素,带了,表示剩余元素,这就导致元组的推断,其实比字符的要简单非常多。
type Case<T extends unknown[]> = T extends [infer F, ...infer L] ? L : 2;
type Res1 = Case<[1]>; // [],extends 走 true 逻辑,L 为空
type Res2 = Case<[]>; // 2,凑不够一个元素,extends 走 false 逻辑
type Res3 = Case<[1, 2, 3, 4, 5]>; // [2, 3, 4, 5] , extends 走 true 逻辑,L 为 [2, 3, 4, 5]
type Case2<T extends unknown[]> = T extends [infer F, infer M, ...infer L]
? L
: 2;
type Res21 = Case2<[]>; // 2,凑不够两个元素,走 false 逻辑
type Res22 = Case2<[1]>; // 2,凑不够两个元素,走 false 逻辑
type Res23 = Case2<[1, 2]>; // [], extends 走 true 逻辑,L 为空
type Res24 = Case2<[1, 2, 3, 4]>; // [3, 4], extends 走 true 逻辑,L 为 [3, 4]
同时,上述 Case2 可以改变 ...
的位置,来达到 L 获取最后一个元素的方法:
// 此时 返回 L 加上了 [],仅仅是为了同 false 场景下 2 做区分,并无深意
type Case3<T extends unknown[]> = T extends [infer F, ...infer M, infer L]
? [L]
: 2;
type Res31 = Case3<[]>; // 2,凑不够两个元素,走 false 逻辑
type Res32 = Case3<[1]>; // 2,凑不够两个元素,走 false 逻辑
type Res33 = Case3<[1, 2]>; // [2], extends 走 true 逻辑,L 为2,此时 M 为 []
type Res34 = Case3<[1, 2, 3, 4]>; // [4], extends 走 true 逻辑,但是 M 的推断上使用了...,所以 L 只取最后一个,L 为 [4]
目前看,其规则同字符串的操作是一样的。但是由于 ...
操作符的存在,会让数组的类型推断更为准确,如果各处推断都不带 ...
操作符,那么就表示该处仅仅是一个元素,就导致:
// 没有 ... 操作符的存在,就表示只有一个元素的元组才会匹配上,此时只要不是一个元素,都会走 false 逻辑
type Case4<T extends unknown[]> = T extends [infer F] ? [F] : 2;
type Res41 = Case4<[]>; // 2
type Res42 = Case4<[1]>; // [1]
type Res43 = Case4<[1, 2, 3]>; // 2
type Res44 = Case4<[1, 2, 3, 5]>; // 2
# 总结
对于字符串来讲,最后面的 infer 推断会自动抓取剩余的字符进行操作,而元组可以通过扩展操作符指定剩余元素的获取位置。
对于长度不足的情况,如果要推断的元素有 3 个,那么当元素长度不足 2 时(等于 2 时,此时剩余字符及扩展操作符位置会为空字符串 or []
,还是走 true 逻辑),会走 false 逻辑。
同时,对于元组不指定扩展的情况下,数量严格一致才会走 true 逻辑。
当然,还有更细节的,就是 infer x extends xxx
的场景。
由于在字符串中,始终是最后一个 infer 推断会承接剩余的字符,如果最后一个 infer L extends 'xxx'
,那么此时,由于手动给其加了限制,其他的推断也不会自动承接剩余字符。如下:
// 最后一个推断 M extends 了 3, F 不会承接剩余字符,只会占一个坑位
type Case<T> = T extends `${infer F}${infer M extends '3'}` ? F : false;
type Res = Case<'243'>; // false, 因为长度不对,直接走 false 逻辑,原本期望是 F 自动承接剩余字符,但是实际并不是
type Res2 = Case<'23'>; // 2, F 占一个坑,长度对了,走 true 逻辑
// 换成这样,F 因为是最后一个,就会承接剩余字符
type Case<T> = T extends `${infer F}3` ? F : false;
type Res = Case<'243'>; // 24
// 对于中间有元素分割的情况,两边各自执行前述规则,各自的最后一个推断承接剩余字符
type Case5<T extends string> = T extends `${infer F}${infer M}hahaha${infer R}`
? M
: 2;
元组不存在这样的问题,有扩展操作符,就承接剩余的元素(可以为空元组),否则不承接,此时如果长度不对,就走 false
# 用途
了解了这些边界条件,就可以在处理字符 or 元组中游刃有余了。比如遍历字符:
// 没什么实际意义,仅仅用于展示递归遍历
type Traverse<T extends string> = T extends `${infer F}${infer R}`
? `${F}${Traverse<R>}`
: // 为什么是空字符,什么时候会走到这个逻辑。
'';
这里的问题,对于不了解边界的同学来说,就是什么时候会判定为 false 场景,是剩余一个字符?还是只有空字符的场景?
阅读了上文,必然清楚,走到 false,必然是因为 T 是空字符,那么就只需要返回 '' 就好了,此时递归返回上一层的空字符也不会影响结果,而如果返回 never,那么在上一层递归进行拼接的时候就会出现问题。