diff --git "a/TS \347\261\273\345\236\213\344\275\223\346\223\215/249.\347\262\276\350\257\273\343\200\212ObjectEntries, Shift, Reverse...\343\200\213.md" "b/TS \347\261\273\345\236\213\344\275\223\346\223\215/249.\347\262\276\350\257\273\343\200\212ObjectEntries, Shift, Reverse...\343\200\213.md" new file mode 100644 index 00000000..a8fdf853 --- /dev/null +++ "b/TS \347\261\273\345\236\213\344\275\223\346\223\215/249.\347\262\276\350\257\273\343\200\212ObjectEntries, Shift, Reverse...\343\200\213.md" @@ -0,0 +1,306 @@ +解决 TS 问题的最好办法就是多练,这次解读 [type-challenges](https://github.com/type-challenges/type-challenges) Medium 难度 41~48 题。 + +## 精读 + +### [ObjectEntries](https://github.com/type-challenges/type-challenges/blob/main/questions/02946-medium-objectentries/README.md) + +实现 TS 版本的 `Object.entries`: + +```ts +interface Model { + name: string; + age: number; + locations: string[] | null; +} +type modelEntries = ObjectEntries // ['name', string] | ['age', number] | ['locations', string[] | null]; +``` + +经过前面的铺垫,大家应该熟悉了 TS 思维思考问题,这道题看到后第一个念头应该是:如何先把对象转换为联合类型?这个问题不解决,就无从下手。 + +对象或数组转联合类型的思路都是类似的,一个数组转联合类型用 `[number]` 作为下标: + +```ts +['1', '2', '3']['number'] // '1' | '2' | '3' +``` + +对象的方式则是 `[keyof T]` 作为下标: + +```ts +type ObjectToUnion = T[keyof T] +``` + +再观察这道题,联合类型每一项都是数组,分别是 Key 与 Value,这样就比较好写了,我们只要构造一个 Value 是符合结构的对象即可: + +```ts +type ObjectEntries = { + [K in keyof T]: [K, T[K]] +}[keyof T] +``` + +为了通过单测 `ObjectEntries<{ key?: undefined }>`,让 Key 位置不出现 `undefined`,需要强制把对象描述为非可选 Key: + +```TS +type ObjectEntries = { + [K in keyof T]-?: [K, T[K]] +}[keyof T] +``` + +为了通过单测 `ObjectEntries>`,得将 Value 中 `undefined` 移除: + +```ts +// 本题答案 +type RemoveUndefined = [T] extends [undefined] ? T : Exclude +type ObjectEntries = { + [K in keyof T]-?: [K, RemoveUndefined] +}[keyof T] +``` + +### [Shift](https://github.com/type-challenges/type-challenges/blob/main/questions/03062-medium-shift/README.md) + +实现 TS 版 `Array.shift`: + +```ts +type Result = Shift<[3, 2, 1]> // [2, 1] +``` + +这道题应该是简单难度的,只要把第一项抛弃即可,利用 `infer` 轻松实现: + +```ts +// 本题答案 +type Shift = T extends [infer First, ...infer Rest] ? Rest : never +``` + +### [Tuple to Nested Object](https://github.com/type-challenges/type-challenges/blob/main/questions/03188-medium-tuple-to-nested-object/README.md) + +实现 `TupleToNestedObject`,其中 `T` 仅接收字符串数组,`P` 是任意类型,生成一个递归对象结构,满足如下结果: + +```ts +type a = TupleToNestedObject<['a'], string> // {a: string} +type b = TupleToNestedObject<['a', 'b'], number> // {a: {b: number}} +type c = TupleToNestedObject<[], boolean> // boolean. if the tuple is empty, just return the U type +``` + +这道题用到了 5 个知识点:递归、辅助类型、`infer`、如何指定对象 Key、`PropertyKey`,你得全部知道并组合起来才能解决该题。 + +首先因为返回值是个递归对象,递归过程中必定不断修改它,因此给泛型添加第三个参数 `R` 存储这个对象,并且在递归数组时从最后一个开始,这样从最内层对象开始一点点把它 “包起来”: + +```ts +type TupleToNestedObject = /** 伪代码 + T extends [...infer Rest, infer Last] +*/ +``` + +下一步是如何描述一个对象 Key?之前 `Chainable Options` 例子我们学到的 `K in Q`,但需要注意直接这么写会报错,因为必须申明 `Q extends PropertyKey`。最后再处理一下递归结束条件,即 `T` 变成空数组时直接返回 `R`: + +```ts +// 本题答案 +type TupleToNestedObject = T extends [] ? R : ( + T extends [...infer Rest, infer Last extends PropertyKey] ? ( + TupleToNestedObject + ) : never +) +``` + +### [Reverse](https://github.com/type-challenges/type-challenges/blob/main/questions/03192-medium-reverse/README.md) + +实现 TS 版 `Array.reverse`: + +```ts +type a = Reverse<['a', 'b']> // ['b', 'a'] +type b = Reverse<['a', 'b', 'c']> // ['c', 'b', 'a'] +``` + +这道题比上一题简单,只需要用一个递归即可: + +```ts +// 本题答案 +type Reverse = T extends [...infer Rest, infer End] ? [End, ...Reverse] : T +``` + +### [Flip Arguments](https://github.com/type-challenges/type-challenges/blob/main/questions/03196-medium-flip-arguments/README.md) + +实现 `FlipArguments` 将函数 `T` 的参数反转: + +```ts +type Flipped = FlipArguments<(arg0: string, arg1: number, arg2: boolean) => void> +// (arg0: boolean, arg1: number, arg2: string) => void +``` + +本题与上题类似,只是反转内容从数组变成了函数的参数,只要用 `infer` 定义出函数的参数,利用 `Reverse` 函数反转一下即可: + +```ts +// 本题答案 +type Reverse = T extends [...infer Rest, infer End] ? [End, ...Reverse] : T + +type FlipArguments = + T extends (...args: infer Args) => infer Result ? (...args: Reverse) => Result : never +``` + +### [FlattenDepth](https://github.com/type-challenges/type-challenges/blob/main/questions/03243-medium-flattendepth/README.md) + +实现指定深度的 Flatten: + +```ts +type a = FlattenDepth<[1, 2, [3, 4], [[[5]]]], 2> // [1, 2, 3, 4, [5]]. flattern 2 times +type b = FlattenDepth<[1, 2, [3, 4], [[[5]]]]> // [1, 2, 3, 4, [[5]]]. Depth defaults to be 1 +``` + +这道题比之前的 `Flatten` 更棘手一些,因为需要控制打平的次数。 + +基本想法就是,打平 `Deep` 次,所以需要实现打平一次的函数,再根据 `Deep` 值递归对应次: + +```ts +type FlattenOnce = T extends [infer X, ...infer Y] ? ( + X extends any[] ? FlattenOnce : FlattenOnce +) : U +``` + +然后再实现主函数 `FlattenDepth`,因为 TS 无法实现 +、- 号运算,我们必须用数组长度判断与操作数组来辅助实现: + +```ts +// FlattenOnce +type FlattenDepth< + T extends any[], + U extends number = 1, + P extends any[] = [] +> = P['length'] extends U ? T : ( + FlattenDepth, U, [...P, any]> +) +``` + +当递归没有达到深度 `U` 时,就用 `[...P, any]` 的方式给数组塞一个元素,下次如果能匹配上 `P['length'] extends U` 说明递归深度已达到。 + +但考虑到测试用例 `FlattenDepth<[1, [2, [3, [4, [5]]]]], 19260817>` 会引发超长次数递归,需要提前终止,即如果打平后已经是平的,就不用再继续递归了,此时可以用 `FlattenOnce extends T` 判断: + +```ts +// 本题答案 +// FlattenOnce +type FlattenDepth< + T extends any[], + U extends number = 1, + P extends any[] = [] +> = P['length'] extends U ? T : ( + FlattenOnce extends T ? T : ( + FlattenDepth, U, [...P, any]> + ) +) +``` + +### [BEM style string](https://github.com/type-challenges/type-challenges/blob/main/questions/03326-medium-bem-style-string/README.md) + +实现 `BEM` 函数完成其规则拼接: + +```ts +Expect, 'btn--small' | 'btn--medium' | 'btn--large' >>, +``` + +之前我们了解了通过下标将数组或对象转成联合类型,这里还有一个特殊情况,即字符串中通过这种方式申明每一项,会自动笛卡尔积为新的联合类型: + +```ts +type BEM = + `${B}__${E[number]}--${M[number]}` +``` + +这是最简单的写法,但没有考虑项不存在的情况。不如创建一个 `SafeUnion` 函数,当传入值不存在时返回空字符串,保证安全的跳过: + +```ts +type IsNever = TValue[] extends never[] ? true : false; +type SafeUnion = IsNever extends true ? "" : TUnion; +``` + +最终代码: + +```ts +// 本题答案 +// IsNever, SafeUnion +type BEM = + `${B}${SafeUnion<`__${E[number]}`>}${SafeUnion<`--${M[number]}`>}` +``` + +### [InorderTraversal](https://github.com/type-challenges/type-challenges/blob/main/questions/03376-medium-inordertraversal/README.md) + +实现 TS 版二叉树中序遍历: + +```ts +const tree1 = { + val: 1, + left: null, + right: { + val: 2, + left: { + val: 3, + left: null, + right: null, + }, + right: null, + }, +} as const + +type A = InorderTraversal // [1, 3, 2] +``` + +首先回忆一下二叉树中序遍历 JS 版的实现: + +```js +function inorderTraversal(tree) { + if (!tree) return [] + return [ + ...inorderTraversal(tree.left), + res.push(val), + ...inorderTraversal(tree.right) + ] +} +``` + +对 TS 来说,实现递归的方式有一点点不同,即通过 `extends TreeNode` 来判定它不是 Null 从而递归: + +```ts +// 本题答案 +interface TreeNode { + val: number + left: TreeNode | null + right: TreeNode | null +} +type InorderTraversal = [T] extends [TreeNode] ? ( + [ + ...InorderTraversal, + T['val'], + ...InorderTraversal + ] +): [] +``` + +你可能会问,问什么不能像 JS 一样,用 `null` 做判断呢? + +```ts +type InorderTraversal = [T] extends [null] ? [] : ( + [ // error + ...InorderTraversal, + T['val'], + ...InorderTraversal + ] +) +``` + +如果这么写会发现 TS 抛出了异常,因为 TS 不能确定 `T` 此时符合 `TreeNode` 类型,所以要执行操作时一般采用正向判断。 + +## 总结 + +这些类型挑战题目需要灵活组合 TS 的基础知识点才能破解,常用的包括: + +- 如何操作对象,增减 Key、只读、合并为一个对象等。 +- 递归,以及辅助类型。 +- `infer` 知识点。 +- 联合类型,如何从对象或数组生成联合类型,字符串模板与联合类型的关系。 + +> 讨论地址是:[精读《ObjectEntries, Shift, Reverse...》· Issue #431 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/431) + +**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。** + +> 关注 **前端精读微信公众号** + + + +> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)) diff --git a/readme.md b/readme.md index 15c531da..8edf6835 100644 --- a/readme.md +++ b/readme.md @@ -6,7 +6,7 @@ 前端界的好文精读,每周更新! -最新精读:248.精读《MinusOne, PickByType, StartsWith...》 +最新精读:249.精读《ObjectEntries, Shift, Reverse...》 素材来源:[周刊参考池](https://github.com/ascoders/weekly/issues/2) @@ -200,6 +200,7 @@ - 246.精读《Permutation, Flatten, Absolute...》 - 247.精读《Diff, AnyOf, IsUnion...》 - 248.精读《MinusOne, PickByType, StartsWith...》 +- 249.精读《ObjectEntries, Shift, Reverse...》 ### 设计模式