# 基操-类型推断的边界条件

在 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,那么在上一层递归进行拼接的时候就会出现问题。

Last Updated: 2023/5/16 06:00:28