热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

精读《Permutation,Flatten,Absolute...》

精读《Permutation,Flatten,Absolute》-解决TS问题的最好办法就是多练,这次解读type-challengesMedium难度17~24题。精读

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

精读

Permutation

实现 Permutation 类型,将联合类型替换为可能的全排列:

type perm = Permutation<'A' | 'B' | 'C'>; // ['A', 'B', 'C'] | ['A', 'C', 'B'] | ['B', 'A', 'C'] | ['B', 'C', 'A'] | ['C', 'A', 'B'] | ['C', 'B', 'A']

看到这题立马联想到 TS 对多个联合类型泛型处理是采用分配律的,在第一次做到 Exclude 题目时遇到过:

Exclude<'a' | 'b', 'a' | 'c'>
// 等价于
Exclude<'a', 'a' | 'c'> | Exclude<'b', 'a' | 'c'>

所以这题如果能 “递归触发联合类型分配率”,就有戏解决啊。但触发的条件必须存在两个泛型,而题目传入的只有一个,我们只好创造第二个泛型,使其默认值等于第一个:

type Permutation

这样对本题来说,会做如下展开:

Permutation<'A' | 'B' | 'C'>
// 等价于
Permutation<'A' | 'B' | 'C', 'A' | 'B' | 'C'>
// 等价于
Permutation<'A', 'A' | 'B' | 'C'> | Permutation<'B', 'A' | 'B' | 'C'> | Permutation<'C', 'A' | 'B' | 'C'>

对于 Permutation<'A', 'A' | 'B' | 'C'> 来说,排除掉对自身的组合,可形成 'A', 'B''A', 'C' 组合,之后只要再递归一次,再拼一次,把已有的排除掉,就形成了 A 的全排列,以此类推,形成所有字母的全排列。

这里要注意两点:

  1. 如何排除掉自身?Exclude 正合适,该函数遇到 T 在联合类型 P 中时,会返回 never,否则返回 T
  2. 递归何时结束?每次递归时用 Exclude 留下没用过的组合,最后一次组合用完一定会剩下 never,此时终止递归。
// 本题答案
type Permutation = [T] extends [never] ? [] : T extends U ? [T, ...Permutation>] : []

验证一下答案,首先展开 Permutation<'A', 'B', 'C'>

'A' extends 'A' | 'B' | 'C' ? ['A', ...Permutation<'B' | 'C'>] : []
'B' extends 'A' | 'B' | 'C' ? ['B', ...Permutation<'A' | 'C'>] : []
'C' extends 'A' | 'B' | 'C' ? ['C', ...Permutation<'A' | 'B'>] : []

我们再展开第一行 Permutation<'B' | 'C'>

'B' extends 'B' | 'C' ? ['B', ...Permutation<'C'>] : []
'C' extends 'B' | 'C' ? ['C', ...Permutation<'B'>] : []

再展开第一行的 Permutation<'C'>:

'C' extends 'C' ? ['C', ...Permutation] : []

此时已经完成全排列,但我们还要处理一下 Permutation,使其返回 [] 并终止递归。那为什么要用 [T] extends [never] 而不是 T extends never 呢?

如果我们用 T extends never 代替本题答案,输出结果是 never,原因如下:

type X = never extends never ? 1 : 0 // 1

type Custom = T extends never ? 1 : 0
type Y = Custom // never

理论上相同的代码,为什么用泛型后输出就变成 never 了呢?原因是 TS 在做 T extends never ? 时,会对联合类型进行分配,此时有一个特例,即当 T = never 时,会跳过分配直接返回 T 本身,所以三元判断代码实际上没有执行。

[T] extends [never] 这种写法可以避免 TS 对联合类型进行分配,继而绕过上面的问题。

Length of String

实现 LengthOfString 返回字符串 T 的长度:

LengthOfString<'abc'> // 3

破解此题你需要知道一个前提,即 TS 访问数组类型的 [length] 属性可以拿到长度值:

['a','b','c']['length'] // 3

也就是说,我们需要把 'abc' 转化为 ['a', 'b', 'c']

第二个需要了解的前置知识是,用 infer 指代字符串时,第一个指代指向第一个字母,第二个指向其余所有字母:

'abc' extends `${infer S}${infer E}` ? S : never // 'a'

那转换后的数组存在哪呢?类似 js,我们弄第二个默认值泛型存储即可:

// 本题答案
type LengthOfString = S extends `${infer S}${infer E}` ? LengthOfString : N['length']

思路就是,每次把字符串第一个字母拿出来放到数组 N 的第一项,直到字符串被取完,直接拿此时的数组长度。

Flatten

实现类型 Flatten:

type flatten = Flatten<[1, 2, [3, 4], [[[5]]]]> // [1, 2, 3, 4, 5]

此题一看就需要递归:

// 本题答案
type Flatten = T extends [infer Start, ...infer Rest] ? (
  Start extends any[] ? Flatten]> : Flatten
) : Result

这道题看似答案复杂,其实还是用到了上一题的套路:递归时如果需要存储临时变量,用泛型默认值来存储

本题我们就用 Result 这个泛型存储打平后的结果,每次拿到数组第一个值,如果第一个值不是数组,则直接存进去继续递归,此时 T 自然是剩余的 Rest;如果第一个值是数组,则将其打平,此时有个精彩的地方,即 ...Start 打平后依然可能是数组,比如 [[5]] 就套了两层,能不能想到 ...Flatten 继续复用递归是解题关键。

Append to object

实现 AppendToObject:

type Test = { id: '1' }
type Result = AppendToObject // expected to be { id: '1', value: 4 }

结合之前刷题的经验,该题解法很简单,注意 K in Key 可以给对象拓展某些指定 Key:

// 本题答案
type AppendToObject = Obj & {
  [K in Key]: Value
}

当然也有不用 Obj & 的写法,即把原始对象和新 Key, Value 合在一起的描述方式:

// 本题答案
type AppendToObject = {
  [key in (keyof T) | U]: key extends U ? V : T[Exclude]
}

Absolute

实现 Absolute 将数字转成绝对值:

type Test = -100;
type Result = Absolute; // expected to be "100"

该题重点是把数字转成绝对值字符串,所以我们可以用字符串的方式进行匹配:

// 本题答案
type Absolute = `${T}` extends `-${infer R}` ? R : `${T}`

为什么不用 T extends 来判断呢?因为 T 是数字,这样写无法匹配符号的字符串描述。

String to Union

实现 StringToUnion 将字符串转换为联合类型:

type Test = '123';
type Result = StringToUnion; // expected to be "1" | "2" | "3"

还是老套路,用一个新的泛型存储答案,递归即可:

// 本题答案
type StringToUnion = T extends `${infer F}${infer R}` ? StringToUnion : P

当然也可以不依托泛型存储答案,因为该题比较特殊,可以直接用 |

// 本题答案
type StringToUnion = T extends `${infer F}${infer R}` ? F | StringToUnion : never

Merge

实现 Merge 合并两个对象,冲突时后者优先:

type foo = {
  name: string;
  age: string;
}
type coo = {
  age: number;
  sex: string
}

type Result = Merge; // expected to be {name: string, age: number, sex: string}

这道题答案甚至是之前题目的解题步骤,即用一个对象描述 + keyof 的思维:

// 本题答案
type Merge = {
  [K in keyof A | keyof B] : K extends keyof B ? B[K] : (
    K extends keyof A ? A[K] : never
  )
}

只要知道 in keyof 支持元组,值部分用 extends 进行区分即可,很简单。

KebabCase

实现驼峰转横线的函数 KebabCase:

KebabCase<'FooBarBaz'> // 'foo-bar-baz'

还是老套路,用第二个参数存储结果,用递归的方式遍历字符串,遇到大写字母就转成小写并添加上 -,最后把开头的 - 干掉就行了:

// 本题答案
type KebabCase = S extends `${infer F}${infer R}` ? (
  Lowercase extends F ? KebabCase : KebabCase}`>
) : RemoveFirstHyphen

type RemoveFirstHyphen = S extends `-${infer Rest}` ? Rest : S

分开写就非常容易懂了,首先 KebabCase 每次递归取第一个字符,如何判断这个字符是大写呢?只要小写不等于原始值就是大写,所以判断条件就是 Lowercase extends F 的 false 分支。然后再写个函数 RemoveFirstHyphen 把字符串第一个 - 干掉即可。

总结

TS 是一门编程语言,而不是一门简单的描述或者修饰符,很多复杂类型问题要动用逻辑思维来实现,而不是查查语法就能简单实现。

讨论地址是:精读《Permutation, Flatten, Absolute...》· Issue #426 · dt-fe/weekly

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

版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证)


推荐阅读
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • 本文介绍了lua语言中闭包的特性及其在模式匹配、日期处理、编译和模块化等方面的应用。lua中的闭包是严格遵循词法定界的第一类值,函数可以作为变量自由传递,也可以作为参数传递给其他函数。这些特性使得lua语言具有极大的灵活性,为程序开发带来了便利。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 本文介绍了C#中数据集DataSet对象的使用及相关方法详解,包括DataSet对象的概述、与数据关系对象的互联、Rows集合和Columns集合的组成,以及DataSet对象常用的方法之一——Merge方法的使用。通过本文的阅读,读者可以了解到DataSet对象在C#中的重要性和使用方法。 ... [详细]
  • C语言注释工具及快捷键,删除C语言注释工具的实现思路
    本文介绍了C语言中注释的两种方式以及注释的作用,提供了删除C语言注释的工具实现思路,并分享了C语言中注释的快捷键操作方法。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • Android JSON基础,音视频开发进阶指南目录
    Array里面的对象数据是有序的,json字符串最外层是方括号的,方括号:[]解析jsonArray代码try{json字符串最外层是 ... [详细]
  • 利用Visual Basic开发SAP接口程序初探的方法与原理
    本文介绍了利用Visual Basic开发SAP接口程序的方法与原理,以及SAP R/3系统的特点和二次开发平台ABAP的使用。通过程序接口自动读取SAP R/3的数据表或视图,在外部进行处理和利用水晶报表等工具生成符合中国人习惯的报表样式。具体介绍了RFC调用的原理和模型,并强调本文主要不讨论SAP R/3函数的开发,而是针对使用SAP的公司的非ABAP开发人员提供了初步的接口程序开发指导。 ... [详细]
  • 本文讨论了使用差分约束系统求解House Man跳跃问题的思路与方法。给定一组不同高度,要求从最低点跳跃到最高点,每次跳跃的距离不超过D,并且不能改变给定的顺序。通过建立差分约束系统,将问题转化为图的建立和查询距离的问题。文章详细介绍了建立约束条件的方法,并使用SPFA算法判环并输出结果。同时还讨论了建边方向和跳跃顺序的关系。 ... [详细]
  • 知识图谱——机器大脑中的知识库
    本文介绍了知识图谱在机器大脑中的应用,以及搜索引擎在知识图谱方面的发展。以谷歌知识图谱为例,说明了知识图谱的智能化特点。通过搜索引擎用户可以获取更加智能化的答案,如搜索关键词"Marie Curie",会得到居里夫人的详细信息以及与之相关的历史人物。知识图谱的出现引起了搜索引擎行业的变革,不仅美国的微软必应,中国的百度、搜狗等搜索引擎公司也纷纷推出了自己的知识图谱。 ... [详细]
  • 使用Ubuntu中的Python获取浏览器历史记录原文: ... [详细]
author-avatar
用户19910071
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有