TypeScript类型体操挑战(十五)

121 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情

中等

FlattenDepth(数组扁平)

挑战要求

在线示例

type FlattenDepth<T extends any[], C extends number = 1, U extends any[] = []> =
  T extends [infer F, ...infer R]
  ?
    F extends any[]
    ?
      U['length'] extends C 
      ?
      [F, ...FlattenDepth<R, C, U>]
      : [...FlattenDepth<F, C, [0, ...U]>, ...FlattenDepth<R, C, U>]
    : [F, ...FlattenDepth<R, C, U>]
  : T;
  
// 使用示例:
type a = FlattenDepth<[1, 2, [3, 4], [[[5]]]], 2> // [1, 2, 3, 4, [5]] 扁平层级为 2
type b = FlattenDepth<[1, 2, [3, 4], [[[5]]]]> // [1, 2, 3, 4, [[5]]] 扁平层级为 1

答案参考自解答区👍最多的

下面通过实际的 case 来进行分析,首先标注说明一下:

/*
第一个参数 T:需要处理的数组
第二个参数 C:进行扁平处理的层级,默认值为 1
第三个参数 U:辅助参数,是个数组,该数组的长度等于 C 时,就代表当前迭代值扁平处理完成
*/
type FlattenDepth<T extends any[], C extends number = 1, U extends any[] = []> =
  T extends [infer F, ...infer R]		// 条件 1
  ?
    F extends any[]		// 条件 2
    ?
      U['length'] extends C 	// 条件 3
      ?
      [F, ...FlattenDepth<R, C, U>]
      : [...FlattenDepth<F, C, [0, ...U]>, ...FlattenDepth<R, C, U>]
    : [F, ...FlattenDepth<R, C, U>]
  : T;

第一个 case:

// 没有传入第二个参数,所以默认扁平层级为 1
Expect<Equal<
  FlattenDepth<[1, [2]]>,
  [1, 2]
>>

1. 代入到工具类型当中:

type FlattenDepth<[1, [2]], 1, []> =
  [1, [2]] extends [infer F, ...infer R] 	// 条件 1 成立
  ?
    1 extends any[]		// 条件 2 不成立
    ?
      U['length'] extends C
      ?
        [F, ...FlattenDepth<R, C, U>]
      : [...FlattenDepth<F, C, [0, ...U]>, ...FlattenDepth<R, C, U>]
    // 会走到这里
    : [1, ...FlattenDepth<[[2]], 1, []>]
  : T;

2. 处理数组第二个元素[2],根据上一步的结果,代入到工具类型当中:

type FlattenDepth<[[2]], 1, []> =
  [[2]] extends [infer F, ...infer R] 	// 条件 1 成立
  ?
    [2] extends any[]		// 条件 2 成立
    ?
      0 extends 1 	// 条件 3 不成立
      ?
        [F, ...FlattenDepth<R, C, U>]
      // 会走到这里,第二个元素可以忽略了,因为返回值是空数组 []
      // 所以结果为 [...FlattenDepth<[2], 1, [0]>]
      : [...FlattenDepth<[2], 1, [0]>, ...FlattenDepth<[], 1, []>]
    : [1, ...FlattenDepth<[[2]], 1, U>]
  : T;

2.1 根据上一步的结果,代入到工具类型当中:

type FlattenDepth<[2], 1, [0]> =
  [2] extends [infer F, ...infer R] 	// 条件 1 成立
  ?
    2 extends any[]		// 条件 2 不成立
    ?
      U['length'] extends C
      ?
        [F, ...FlattenDepth<R, C, U>]
      : [...FlattenDepth<[2], 1, [0]>, ...FlattenDepth<[], 1, []>]
    // 会走到这里,第二个元素可以忽略了,因为返回值是空数组 [],所以结果为 [2]
    : [2, ...FlattenDepth<[], 1, [0]>]
  : T;

2 步的最终结果为[...[2]],代入到第一步的结果当中:

[1, ...[...[2]]] => [1, 2]

现在来看看第二个 case:

// 这里有传入第二个参数,值为 2
Expect<Equal<
  FlattenDepth<[1, 2, [3, 4], [[[5]]]], 2>,
  [1, 2, 3, 4, [5]]
>>,

1. 代入到工具类型当中,这里就忽略掉前两个元素了,直接进入到第 3 个元素的处理中:

// 初始结果为:
[1, 2 ...FlattenDepth<[[3, 4], [[[5]]]], 1, []>]

// 代入后:
type FlattenDepth<[[3, 4], [[[5]]]], 2, []> =
  [[3, 4], [[[5]]]] extends [infer F, ...infer R]		// 条件 1 成立
  ?
    [3, 4] extends any[]		// 条件 2 成立
    ?
      0 extends 2 	// 条件 3 不成立
      ?
        [F, ...FlattenDepth<R, C, U>]
      // 会走到这里,这里就不继续深入第一个参数的处理了
      // 所以结果为 [3, 4, ...FlattenDepth<[[[[5]]]], 1, []]
      : [...FlattenDepth<[3, 4], 2, [0]>, ...FlattenDepth<[[[[5]]]], 2, []>]
    : [F, ...FlattenDepth<R, C, U>]
  : T;

2. 根据第 1 步的结果,代入类型:

type FlattenDepth<[[[[5]]]], 2, []> =
  [[[[5]]]] extends [infer F, ...infer R]		// 条件 1 成立
  ?
    [[[5]]] extends any[]		// 条件 2 成立
    ?
      0 extends 2 	// 条件 3 不成立
      ?
        [F, ...FlattenDepth<R, C, U>]
      // 会走到这里,忽略第二个元素,结果为 [...FlattenDepth<[[[5]]], 2, [0]>]
      : [...FlattenDepth<[[[5]]], 2, [0]>, ...FlattenDepth<[], 2, []>]
    : [F, ...FlattenDepth<R, C, U>]
  : T;

2.1 根据上一步的结果,代入类型中:

type FlattenDepth<[[[5]]], 2, [0]> =
  [[[5]]] extends [infer F, ...infer R]		// 条件 1 成立
  ?
    [[5]] extends any[]		// 条件 2 成立
    ?
      1 extends 2 	// 条件 3 不成立
      ?
        [F, ...FlattenDepth<R, C, U>]
      // 会走到这里,忽略第二个元素,结果为 [...FlattenDepth<[[5]], 2, [0, 0]>]
      : [...FlattenDepth<[[5]], 2, [0, 0]>, ...FlattenDepth<[], 2, []>]
    : [F, ...FlattenDepth<R, C, U>]
  : T;

2.2 上一步结果为[...FlattenDepth<[[5]], 2, [0, 0]>],继续代入:

type FlattenDepth<[[5]], 2, [0, 0]> =
  [[5]] extends [infer F, ...infer R]		// 条件 1 成立
  ?
    [5] extends any[]		// 条件 2 成立
    ?
      2 extends 2 	// 条件 3 成立
      ?
        // 会走到这里,忽略第二个元素,结果为 [[5]]
        [F, ...FlattenDepth<R, C, U>]
      : [...FlattenDepth<[[5]], 2, [0, 0]>, ...FlattenDepth<[], 2, []>]
    : [F, ...FlattenDepth<R, C, U>]
  : T;

2.2 的结果为[[5]]代入到 2.1 就是[...[[5]]],再代入到 2 就是 [...[[5]]],再代入到 1 的结果当中:

[1, 2, 3, 4, ...[[5]]] => [1, 2, 3, 4 [5]]

对这两个 case 的执行步骤进行分析后,想必你也清晰很多了吧。

BEM style string(CSS的BEM)

挑战要求

在线示例

type Union<K extends string, T extends string[], C extends string> =
  T extends []
  ?
  K
  : K extends K ? keyof { [P in T[number] as `${K}${C}${P}`]: P } : K;

type BEM<B extends string, E extends string[], M extends string[]> = Union<Union<B, E, '__'>, M, '--'>;

// 使用示例:
BEM<'btn', ['price'], ['warning', 'success']> // 'btn__price--warning' | 'btn__price--success'

在类型Union中,首先要判断目标数组T是否为空,来处理空数组的情况,不为空的情况下才进一步拼接BEM

为啥会有K extends K呢?因为需要借助条件类型产生分布行为,例如这个下面这个 Case:

Expect<Equal<
  BEM<'btn', ['price'], ['warning', 'success']>,
  'btn__price--warning' | 'btn__price--success'
>>

在执行完Union<B, E, '__'>时,这时返回的结果为btn__price,之后拼接需要跟M中的每个元素进行拼接,所以才有了K extends K

我在解答区看到一个简洁的答案:

type BEM<B extends string, E extends string[],M extends string[]> = 
  `${B}${E extends [] ? '' : `__${E[number]}`}${M extends [] ? '' : `--${M[number]}`}`

使用T[number]获取数组的元素时,返回的是联合类型,那么这时搭配字符串拼接,它会与联合类型中的每个类型进行拼接,进行组合,最终返回还是一个联合类型。

当时压根就不记得字符串模板这个特性了🙈