关于javascript:精读Flip-Fibonacci-AllCombinations

34次阅读

共计 6667 个字符,预计需要花费 17 分钟才能阅读完成。

解决 TS 问题的最好方法就是多练,这次解读 type-challenges Medium 难度 49~56 题。

精读

Flip

实现 Flip<T>,将对象 T 中 Key 与 Value 对调:

Flip<{a: "x", b: "y", c: "z"}>; // {x: 'a', y: 'b', z: 'c'}
Flip<{a: 1, b: 2, c: 3}>; // {1: 'a', 2: 'b', 3: 'c'}
Flip<{a: false, b: true}>; // {false: 'a', true: 'b'}

keyof 形容对象时能够通过 as 追加变形,所以这道题应该这样解决:

type Flip<T> = {[K in keyof T as T[K]]: K
}

因为 Key 地位只能是 String or Number,所以 T[K] 形容 Key 会显示谬误,咱们须要限定 Value 的类型:

type Flip<T extends Record<string, string | number>> = {[K in keyof T as T[K]]: K
}

但这个答案无奈通过测试用例 Flip<{pi: 3.14; bool: true}>,起因是 true 不能作为 Key。只能用字符串 'true' 作为 Key,所以咱们得强行把 Key 地位转化为字符串:

// 本题答案
type Flip<T extends Record<string, string | number | boolean>> = {[K in keyof T as `${T[K]}`]: K
}

Fibonacci Sequence

用 TS 实现斐波那契数列计算:

type Result1 = Fibonacci<3> // 2
type Result2 = Fibonacci<8> // 21

因为测试用例没有特地大的 Case,咱们能够释怀用递归实现。JS 版的斐波那契十分天然,但 TS 版咱们只能用数组长度模拟计算,代码写起来天然会比拟扭曲。

首先须要一个额定变量标记递归了多少次,递归到第 N 次完结:

type Fibonacci<T extends number, N = [1]> = N['length'] extends T ? (// xxx) : Fibonacci<T, [...N, 1]>

下面代码每次执行都判断是否递归实现,否则持续递归并把计数器加一。咱们还须要一个数组存储答案,一个数组存储上一个数:

// 本题答案
type Fibonacci<
  T extends number,
  N extends number[] = [1],
  Prev extends number[] = [1],
  Cur extends number[] = [1]
> = N['length'] extends T
  ? Prev['length']
  : Fibonacci<T, [...N, 1], Cur, [...Prev, ...Cur]>

递归时拿 Cur 代替下次的 Prev,用 [...Prev, ...Cur] 代替下次的 Cur,也就是说,下次的 Cur 合乎斐波那契定义。

AllCombinations

实现 AllCombinations<S> 对字符串 S 全排列:

type AllCombinations_ABC = AllCombinations<'ABC'>
// should be ''|'A'|'B'|'C'|'AB'|'AC'|'BA'|'BC'|'CA'|'CB'|'ABC'|'ACB'|'BAC'|'BCA'|'CAB'|'CBA'

首先要把 ABC 字符串拆成一个个独立的联结类型,进行二次组合才可能实现全排列:

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

infer 形容字符串时,第一个指向第一个字母,第二个指向残余字母;对残余字符串递归能够将其逐个拆解为单个字符并用 | 连贯:

StrToUnion<'ABC'> // 'A' | 'B' | 'C'

StrToUnion<'ABC'> 的后果记为 U,则利用对象转联结类型特色,能够制作出 ABC 在三个字母时的全排列:

{[K in U]: `${K}${AllCombinations<never, Exclude<U, K>>}` }[U] // `ABC${any}` | `ACB${any}` | `BAC${any}` | `BCA${any}` | `CAB${any}` | `CBA${any}`

然而只有在每次递归时奇妙的加上 '' | 就能够间接失去答案了:

type AllCombinations<S extends string, U extends string = StrToUnion<S>> =
  | ''| {[K in U]: `${K}${AllCombinations<never, Exclude<U, K>>}` }[U] //'' | 'A' | 'B' | 'C' | 'AB' | 'AC' | 'BA' | 'BC' | 'CA' | 'CB' | 'ABC' | 'ACB' | 'BAC' | 'BCA' | 'CAB' | 'CBA'

为什么这么神奇呢?这是因为每次递归时都会经验 '''A''AB''ABC' 这样逐步累加字符的过程,而每次都会遇到 '' | 使其天然造成了联结类型,比方遇到 'A' 时,会天然造成 'A' 这项联结类型,同时持续用 'A'Exclude<'A' | 'B' | 'C', 'A'> 进行组合。

更精妙的是,第一次执行时的 '' 填补了全排列的第一个 Case。

最初留神到下面的后果产生了一个 Error:”Type instantiation is excessively deep and possibly infinite”,即这样递归可能产生死循环,因为 Exclude<U, K> 的后果可能是 never,所以最初在结尾修补一下对 never 的判否,利用之前学习的常识,never 不会进行联结类型开展,所以咱们用 [never] 判断来躲避:

// 本题答案
type AllCombinations<S extends string, U extends string = StrToUnion<S>> = [U] extends [never]
  ? '':'' | {[K in U]: `${K}${AllCombinations<never, Exclude<U, K>>}` }[U]

Greater Than

实现 GreaterThan<T, U> 判断 T > U:

GreaterThan<2, 1> //should be true
GreaterThan<1, 1> //should be false
GreaterThan<10, 100> //should be false
GreaterThan<111, 11> //should be true

因为 TS 不反对加减法与大小判断,看到这道题时就应该想到有两种做法,一种是递归,但会受限于入参数量限度,可能堆栈溢出,一种是参考 MinusOne 的非凡办法,用奇妙的形式结构出长度合乎预期的数组,用数组 ['length'] 进行比拟。

先说第一种,递归必定要有一个递增 Key,拿 T U 先后进行比照,谁先追上这个数,谁就是较小的那个:

// 本题答案
type GreaterThan<T, U, R extends number[] = []> = T extends R['length']
  ? false
  : U extends R['length']
  ? true
  : GreaterThan<T, U, [...R, 1]>

另一种做法是疾速结构两个长度别离等于 T U 的数组,用数组疾速判断谁更长。结构形式不再开展,参考 MinusOne 那篇的办法即可,重点说下如何疾速判断 [1, 1][1, 1, 1] 谁更大。

因为 TS 没有大小判断能力,所以拿到了 ['length'] 也没有用,咱们得思考 arr1 extends arr2 这种形式。惋惜的是,长度不相等的数组,extends 永远等于 false:

[1,1,1,1] extends [1,1,1] ? true : false // false
[1,1,1] extends [1,1,1,1] ? true : false // false
[1,1,1] extends [1,1,1] ? true : false // true

但咱们冀望进行如下判断:

ArrGreaterThan<[1,1,1,1],[1,1,1]> // true
ArrGreaterThan<[1,1,1],[1,1,1,1]> // false
ArrGreaterThan<[1,1,1],[1,1,1]> // false

解决办法十分体现 TS 思维:既然俩数组相等才返回 true,那咱们用 [...T, ...any] 进行补充断定,如果能断定为 true,就阐明前者长度更短(因为后者补充几项后能够判等):

type ArrGreaterThan<T extends 1[], U extends 1[]> = U extends [...T, ...any]
  ? false
  : true

这样一来,第二种答案就是这样的:

// 本题答案
type GreaterThan<T extends number, U extends number> = ArrGreaterThan<
  NumberToArr<T>,
  NumberToArr<U>
>

Zip

实现 TS 版 Zip 函数:

type exp = Zip<[1, 2], [true, false]> // expected to be [[1, true], [2, false]]

此题同样配合辅助变量,进行计数递归,并额定用一个类型变量存储后果:

// 本题答案
type Zip<
  T extends any[],
  U extends any[],
  I extends number[] = [],
  R extends any[] = []
> = I['length'] extends T['length']
  ? R
  : U[I['length']] extends undefined
  ? Zip<T, U, [...I, 0], R>
  : Zip<T, U, [...I, 0], [...R, [T[I['length']], U[I['length']]]]>

[...R, [T[I['length']], U[I['length']]]] 在每次递归时依照 Zip 规定增加一条后果,其中 I['length'] 起到的作用相似 for 循环的下标 i,只是在 TS 语法中,咱们只能用数组的形式模仿这种计数。

IsTuple

实现 IsTuple<T> 判断 T 是否为元组类型(Tuple):

type case1 = IsTuple<[number]> // true
type case2 = IsTuple<readonly [number]> // true
type case3 = IsTuple<number[]> // false

不得不吐槽的是,无论是 TS 外部或者词法解析都是更无效的判断形式,但如果用 TS 来实现,就要换一种思路了。

Tuple 与 Array 在 TS 里的区别是前者长度无限,后者长度有限,从后果来看,如果拜访其 ['length'] 属性,前者肯定是一个固定数字,而后者返回 number,用这个个性判断即可:

// 本题答案
type IsTuple<T> = [T] extends [never]
  ? false
  : T extends readonly any[]
  ? number extends T['length']
    ? false
    : true
  : false

其实这个答案是依据单测一点点试出来的,因为存在 IsTuple<{length: 1}> 单测用例,它能够通过 number extends T['length'] 的校验,但因为其自身不是数组类型,所以无奈通过 T extends readonly any[] 的前置判断。

Chunk

实现 TS 版 Chunk:

type exp1 = Chunk<[1, 2, 3], 2> // expected to be [[1, 2], [3]]
type exp2 = Chunk<[1, 2, 3], 4> // expected to be [[1, 2, 3]]
type exp3 = Chunk<[1, 2, 3], 1> // expected to be [[1], [2], [3]]

老办法还是要递归,须要一个变量记录以后收集到 Chunk 里的内容,在 Chunk 达到下限时释放出来,同时也要留神未达到下限就完结时也要释放出来。

type Chunk<
  T extends any[],
  N extends number = 1,
  Chunked extends any[] = []
> = T extends [infer First, ...infer Last]
  ? Chunked['length'] extends N
    ? [Chunked, ...Chunk<T, N>]
    : Chunk<Last, N, [...Chunked, First]>
  : [Chunked]

Chunked['length'] extends N 判断 Chunked 数组长度达到 N 后就释放出来,否则把以后数组第一项 First 持续塞到 Chunked 数组,数组项从 Last 开始持续递归。

咱们发现 Chunk<[], 1> 这个单测没过,因为当 Chunked 没有我的项目时,就无需成组了,所以残缺的答案是:

// 本题答案
type Chunk<
  T extends any[],
  N extends number = 1,
  Chunked extends any[] = []
> = T extends [infer Head, ...infer Tail]
  ? Chunked['length'] extends N
    ? [Chunked, ...Chunk<T, N>]
    : Chunk<Tail, N, [...Chunked, Head]>
  : Chunked extends []
  ? Chunked
  : [Chunked]

Fill

实现 Fill<T, N, Start?, End?>,将数组 T 的每一项替换为 N

type exp = Fill<[1, 2, 3], 0> // expected to be [0, 0, 0]

这道题也须要用递归 + Flag 形式解决,即定义一个 I 示意以后递归的下标,一个 Flag 示意是否到了要替换的下标,只有到了这个下标,该 Flag 就永远为 true

type Fill<
  T extends unknown[],
  N,
  Start extends number = 0,
  End extends number = T['length'],
  I extends any[] = [],
  Flag extends boolean = I['length'] extends Start ? true : false
>

因为递归会一直生成残缺答案,咱们将 T 定义为可变的,即每次仅解决第一条,如果以后 Flagtrue 就采纳替换值 N,否则就拿本来的第一个字符:

type Fill<
  T extends unknown[],
  N,
  Start extends number = 0,
  End extends number = T['length'],
  I extends any[] = [],
  Flag extends boolean = I['length'] extends Start ? true : false
> = I['length'] extends End
  ? T
  : T extends [infer F, ...infer R]
  ? Flag extends false
    ? [F, ...Fill<R, N, Start, End, [...I, 0]>]
    : [N, ...Fill<R, N, Start, End, [...I, 0]>]
  : T

但这个答案没有通过测试,认真想想发现 FlagI 长度超过 Start 后就断定失败了,为了让超过后维持 true,在 Flagtrue 时将其传入笼罩后续值即可:

// 本题答案
type Fill<
  T extends unknown[],
  N,
  Start extends number = 0,
  End extends number = T['length'],
  I extends any[] = [],
  Flag extends boolean = I['length'] extends Start ? true : false
> = I['length'] extends End
  ? T
  : T extends [infer F, ...infer R]
  ? Flag extends false
    ? [F, ...Fill<R, N, Start, End, [...I, 0]>]
    : [N, ...Fill<R, N, Start, End, [...I, 0], Flag>]
  : T

总结

探讨地址是:精读《Flip, Fibonacci, AllCombinations…》· Issue #432 · dt-fe/weekly

如果你想参加探讨,请 点击这里,每周都有新的主题,周末或周一公布。前端精读 – 帮你筛选靠谱的内容。

关注 前端精读微信公众号

<img width=200 src=”https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg”>

版权申明:自在转载 - 非商用 - 非衍生 - 放弃署名(创意共享 3.0 许可证)

正文完
 0