TypeScript类型体操挑战(二十三)

146 阅读2分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第5天,点击查看活动详情

中等

Combination

挑战要求
在线示例

type Combination<T extends string[], All = T[number], Item = All> = 
  Item extends (infer Item extends string)
  ? Item | `${Item} ${Combination<[], Exclude<All, Item>>}`
  : never


/* 
expected to be `
  "foo" | "bar" | "baz" | "foo bar" | "foo bar baz" |
  "foo baz" | "foo baz bar" | "bar foo" | "bar foo baz" | "bar baz" | 
  "bar baz foo" | "baz foo" | "baz foo bar" | "baz bar" | "baz bar foo"
`
*/
type Keys = Combination<['foo', 'bar', 'baz']>

答案参考自解答区

infer Item extends string是一个整体,用于推断Item的类型,并限制类型为string。假如不这么写,就得这样:

type Combination<T extends string[], All = T[number], Item = All> = 
  Item extends Item
  // 需要转换类型
  ? Item | `${Item & string} ${Combination<[], Exclude<All, Item>> & string}`
  : never

所以说,就是使用inferItem的类型给推断出来了。


下面通过一个 case 进行剖析,了解其运行原理。

1. 代入Combination<['foo', 'bar', 'baz']>

type Combination<['foo', 'bar', 'baz'], All = 'foo' | 'bar' | 'baz', Item = 'foo' | 'bar' | 'baz'> = 
  Item extends (infer Item extends string)
  ? Item | `${Item} ${Combination<[], Exclude<All, Item>>}`
  : never

Item的值为联合类型,那么用在添加类型中时,会发生分布行为,就会对Item中每个值都进行处理。

2. 代入第一个值'foo'

type Combination<['foo', 'bar', 'baz'], All = 'foo' | 'bar' | 'baz', Item = 'foo' | 'bar' | 'baz'> = 
  'foo' extends 'foo' | 'bar' | 'baz'
  // Exclude 的结果为 'bar' | 'baz'
  ? 'foo' | `foo ${Combination<[], Exclude<'foo' | 'bar' | 'baz', 'foo'>>}`
  : never

执行后结果为'foo' | `foo ${Combination<...>}`

3. 根据上一步的结果,继续代入Combination

由于Item还是联合类型,所以这里又发生了分布行为,这里代入第一个值'bar'

type Combination<[], All = 'bar' | 'baz', Item = 'bar' | 'baz'> = 
  'bar' extends 'bar' | 'baz'
  // Exclude 的结果为 'baz'
  ? 'bar' | `bar ${Combination<[], Exclude<'bar' | 'baz', 'bar'>>}`
  : never

执行后结果为'foo' | `foo ${'bar' | Combination<...>}`

4. 根据上一步的结果,继续代入Combination

type Combination<[], All = 'baz', Item = 'baz'> = 
  'baz' extends 'baz'
  // Exclude 的结果为 never
  ? 'baz' | `baz ${Combination<[], Exclude<'baz', 'baz'>>}`
  : never

到这里就结束了,执行后结果为'foo' | `foo ${'bar' | `bar ${'baz'}`}`

由于模板字符串的特性,也就变成了:

'foo' | 'foo bar' | 'foo bar baz'
// 要是把 foo 后面的结果完整代入:
'foo' | 'foo bar' | 'foo bar baz' | 'foo baz' | 'foo baz bar'

数组中每个元素都能组成 5 个类型,最终结果就是 15 个类型,跟最上面调用示例中展示的一样。

Subsequence

挑战要求
在线示例

type Medium<T, U, R extends any[] = [[U]]> =
  T extends [infer F, ...infer E] 
    ? Medium<E, U, [...R, [U, F], [U, ...T]]> 
  : R;

type Subsequence<T extends any[], R extends any[] = [[]]> =
  T extends [infer F, ...infer E] 
    ? Subsequence<E, [...R, ...Medium<E, F>]> 
  : R[number];


// 示例:
type A = Subsequence<[1, 2]> // [] | [1] | [2] | [1, 2]

画图解释:

解答区看到一个优雅简洁的解答,没想到联合类型还能这么用:

type Subsequence<T extends any[], Prefix extends any[] = []> = 
  T extends [infer F, ...infer R] 
    ? Subsequence<R, Prefix | [...Prefix, F]> 
  : Prefix

主要关注下 Prefix | [...Prefix, F],来看个例子就明白了:

type A<Prefix extends any[] = [] | [1]> = [...Prefix, 2];
// 代入一下,得到结果:[2] | [1, 2]
type A = [...([] | [1]), 2];

由于Prefix是个联合类型,所以拆成两步:

  1. [...[], 2]得到 [2]
  2. [...[1], 2]得到 [1, 2]

因为是联合类型,所以会有两个结果,最后也就得到了[2] | [1, 2]