关于前端:精读type-challenges-easy

69次阅读

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

TS 强类型十分好用,但在理论使用中,免不了遇到一些难以描述,重复看官网文档也解决不了的问题,至今为止也没有任何一篇文档,或者一套教材能够解决所有犄角旮旯的类型问题。为什么会这样呢?因为 TS 并不是简略的正文器,而是一门图灵齐备的语言,所以很多问题的解决办法藏在根底能力里,但你学会了根底能力又不肯定能想到这么用。

解决该问题的最好方法就是多练,通过理论案例一直刺激你的大脑,让你养成 TS 思维习惯。所以话不多说,咱们明天从 type-challenges 的 Easy 难度题目开始吧。

精读

Pick

手动实现内置 Pick<T, K> 函数,返回一个新的类型,从对象 T 中抽取类型 K:

interface Todo {
  title: string
  description: string
  completed: boolean
}

type TodoPreview = MyPick<Todo, 'title' | 'completed'>

const todo: TodoPreview = {
    title: 'Clean room',
    completed: false,
}

联合例子更容易看明确,也就是 K 是一个字符串,咱们须要返回一个新类型,仅保留 K 定义的 Key。

第一个难点在如何限度 K 的取值,比方传入 T 中不存在的值就要报错。这个考查的是硬常识,只有你晓得 A extends keyof B 这个语法就能联想到。

第二个难点在于如何生成一个仅蕴含 K 定义 Key 的类型,你首先要晓得有 {[A in keyof B]: B[A] } 这个硬常识,这样能够重新组合一个对象:

// 代码 1
type Foo<T> = {[P in keyof T]: T[P]
}

只懂这个语法不肯定能想出思路,起因是你要突破对 TS 的刻板了解,[K in keyof T] 不是一个固定模板,其中 keyof T 只是一个指代变量,它能够被换掉,如果你换掉成另一个范畴的变量,那么这个对象的 Key 值范畴就变了,这正好符合本题的 K

// 代码 2(本题答案)type MyPick<T, K in keyof T> = {[P in K]: T[P]
}

这个题目别看晓得答案后简略,回顾下还是有播种的。比照下面两个代码例子,你会发现,只不过是把代码 1 的 keyof T 从对象形容中提到了泛型定义里而已,所以性能上没有任何变动,但因为泛型能够由用户传入,所以代码 1 的 P in keyof T 因为没有泛型撑持,这里推导进去的就是 T 的所有 Keys,而代码 2 尽管把代码挪到了泛型,但因为用的是 extends 形容,所以示意 P 的类型被束缚到了 T 的 Keys,至于具体是什么,得看用户代码怎么传。

所以其实放到泛型里的 K 是没有默认值的,而写到对象里作为推导值就有了默认值。泛型里给默认值的形式如下:

// 代码 3
type MyPick<T, K extends keyof T = keyof T> = {[P in K]: T[P]
}

也就是说,这样 MyPick<Todo> 就也能够正确工作并一成不变返回 Todo 类型,也就是说,代码 3 在不传第二个参数时,与代码 1 的性能齐全一样。认真推敲一下共同点与区别,为什么代码 3 能够做到和代码 1 性能一样,又有更强的拓展性,你对 TS 泛型的实战了解就上了一个台阶。

Readonly

手动实现内置 Readonly<T> 函数,将对象所有属性设置为只读:

interface Todo {
  title: string
  description: string
}

const todo: MyReadonly<Todo> = {
  title: "Hey",
  description: "foobar"
}

todo.title = "Hello" // Error: cannot reassign a readonly property
todo.description = "barFoo" // Error: cannot reassign a readonly property

这道题反而比第一题简略,只有咱们用 {[A in keyof B]: B[A] } 从新申明对象,并在每个 Key 后面加上 readonly 润饰即可:

// 本题答案
type MyReadonly<T> = {readonly [K in keyof T]: T[K]
}

依据这个个性咱们能够做很多延长革新,比方将对象所有 Key 都设定为可选:

type Optional<T> = {[K in keyof T]?: T[K]
}

{[A in keyof B]: B[A] } 给了咱们形容每一个 Key 属性细节的机会,限度咱们施展的只有想象力。

First Of Array

实现类型 First<T>,取到数组第一项的类型:

type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]

type head1 = First<arr1> // expected to be 'a'
type head2 = First<arr2> // expected to be 3

这题比较简单,很容易想到的答案:

// 本题答案
type First<T extends any[]> = T[0]

但在写这个答案时,有 10% 脑细胞揭示我没有判断边界状况,果然看了下答案,有空数组的状况要思考,空数组时返回类型 never 而不是 undefined 会更好,上面几种写法都是答案:

type First<T extends any[]> = T extends [] ? never : T[0]
type First<T extends any[]> = T['length'] extends 0 ? never : T[0]
type First<T> = T extends [infer P, ...infer Rest] ? P : never

第一种写法通过 extends [] 判断 T 是否为空数组,是的话返回 never

第二种写法通过长度为 0 判断空数组,此时须要了解两点:1. 能够通过 T['length'] 让 TS 拜访到值长度(类型的),2. extends 0 示意是否匹配 0,即 extends 除了匹配类型,还能间接匹配值。

第三种写法是最省心的,但也应用了 infer 关键字,即便你充沛晓得 infer 怎么用(精读《Typescript infer 关键字》),也很难想到它。用 infer 的理由是:该场景存在边界状况,最便于了解的写法是“如果 T 形如 <P, ...>”那我就返回类型 P,否则返回 never”,这句话用 TS 形容就是:T extends [infer P, ...infer Rest] ? P : never

Length of Tuple

实现类型 Length<T> 获取元组长度:

type tesla = ['tesla', 'model 3', 'model X', 'model Y']
type spaceX = ['FALCON 9', 'FALCON HEAVY', 'DRAGON', 'STARSHIP', 'HUMAN SPACEFLIGHT']

type teslaLength = Length<tesla>  // expected 4
type spaceXLength = Length<spaceX> // expected 5

通过上一题的学习,很容易想到这个答案:

type Length<T extends any[]> = T['length']

对 TS 来说,元组和数组都是数组,但元组对 TS 来说能够观测其长度,T['length'] 对元组来说返回的是具体值,而对数组来说返回的是 number

Exclude

实现类型 Exclude<T, U>,返回 T 中不存在于 U 的局部。该性能次要用在联结类型场景,所以咱们间接用 extends 判断就行了:

// 本题答案
type Exclude<T, U> = T extends U ? never : T

理论运行成果:

type C = Exclude<'a' | 'b', 'a' | 'c'> // 'b'

看上去有点不那么好了解,这是因为 TS 对联结类型的执行是分配率的,即:

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

Awaited

实现类型 Awaited,比方从 Promise<ExampleType> 拿到 ExampleType

首先 TS 永远不会执行代码,所以脑子里不要有“await 得等一下才晓得后果”的念头。该题要害就是从 Promise<T> 中抽取类型 T,很适宜用 infer 做:

type MyAwaited<T> = T extends Promise<infer U> ? U : never

然而这个答案还不够规范,标准答案思考了嵌套 Promise 的场景:

// 该题答案
type MyAwaited<T extends Promise<unknown>> = T extends Promise<infer P>
  ? P extends Promise<unknown> ? MyAwaited<P> : P
  : never

如果 Promise<P> 取到的 P 还形如 Promise<unknown>,就递归调用本人 MyAwaited<P>。这里提到了递归,也就是 TS 类型解决能够是递归的,所以才有了前面版本做尾递归优化。

If

实现类型 If<Condition, True, False>,当 Ctrue 时返回 T,否则返回 F

type A = If<true, 'a', 'b'>  // expected to be 'a'
type B = If<false, 'a', 'b'> // expected to be 'b'

之前有提过,extends 还能够用来断定值,所以果决用 extends true 判断是否命中了 true 即可:

// 本题答案
type If<C, T, F> = C extends true ? T : F

Concat

用类型零碎实现 Concat<P, Q>,将两个数组类型连起来:

type Result = Concat<[1], [2]> // expected to be [1, 2]

因为 TS 反对数组解构语法,所以能够大胆的尝试这么写:

type Concat<P extends any[], Q extends any[]> = [...P, ...Q]

思考到 Concat 函数应该也能接管非数组类型,所以做一个判断,为了不便书写,把 extends 从泛型定义地位挪到 TS 类型推断的运行时:

// 本题答案
type Concat<P, Q> = [...P extends any[] ? P : [P],
  ...Q extends any[] ? Q : [Q],
]

解决这题须要信念,置信 TS 能够像 JS 一样写逻辑。这些能力都是版本升级时渐进式提供的,所以须要一直浏览最新 TS 个性,疾速将其了解为固化常识,其实还是有肯定难度的。

Includes

用类型零碎实现 Includes<T, K> 函数:

type isPillarMen = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'> // expected to be `false`

因为之前的教训,很容易做上面的联想:

// 如果题目要求是这样
type isPillarMen = Includes<'Kars' | 'Esidisi' | 'Wamuu' | 'Santana', 'Dio'>
// 那我就能用 extends 轻松解决了
type Includes<T, K> = K extends T ? true : false

惋惜第一个输出是数组类型,extends 可不反对断定“数组蕴含”逻辑,此时要理解一个新知识点,即 TS 判断中的 [number] 下标。不仅这道题,当前很多艰难题都须要它作为基础知识。

[number] 下标示意任意一项,而 extends T[number] 就能够实现数组蕴含的断定,因而上面的解法是无效的:

type Includes<T extends any[], K> = K extends T[number] ? true : false

但翻答案后发现这并不是标准答案,还真找到一个反例:

type Includes<T extends any[], K> = K extends T[number] ? true : false
type isPillarMen = Includes<[boolean], false> // true

起因很简略,truefalse 都继承自 boolean,所以 extends 判断的界线太宽了,题目要求的是准确值匹配,故下面的答案实践上是错的。

标准答案是每次判断数组第一项,并递归(讲真感觉这不是 easy 题),别离有两个难点。

第一如何写 Equal 函数?比拟风行的计划是这个:

type Equal<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? true : false

对于如何写 Equal 函数还引发了一次 小探讨,下面的代码结构了两个函数,这两个函数内的 T 属于 deferred(提早)判断的类型,该类型判断依赖于外部 isTypeIdenticalTo 函数实现判断。

有了 Equal 后就简略了,咱们用解构 + infer + 递归的形式做就能够了:

// 本题答案
type Includes<T extends any[], K> =
  T extends [infer F, ...infer Rest] ?
    Equal<F, K> extends true ?
      true
      : Includes<Rest, K>
    : false

每次取数组第一个值判断 Equal,如果不匹配则拿残余项递归判断。这个函数组合了不少 TS 常识,比方:

  • 递归
  • 解构
  • infer
  • extends true

能够发现,就为了解决 true extends booleantrue 的问题,咱们绕了一大圈应用了更简单的形式来实现,这在 TS 体操中也算是常态,解决问题须要急躁。

Push

实现 Push<T, K> 函数:

type Result = Push<[1, 2], '3'> // [1, 2, '3']

这道题真的很简略,用解构就行了:

// 本题答案
type Push<T extends any[], K> = [...T, K]

可见,想要轻松解决一个 TS 简略问题,首先你须要能解决一些艰难问题 😁。

Unshift

实现 Unshift<T, K> 函数:

type Result = Unshift<[1, 2], 0> // [0, 1, 2,]

Push 根底上改下程序就行了:

// 本题答案
type Unshift<T extends any[], K> = [K, ...T]

Parameters

实现内置函数 Parameters

Parameters 能够拿到函数的参数类型,间接用 infer 实现即可,也比较简单:

type Parameters<T> = T extends (...args: infer P) => any ? P : []

infer 能够很不便从任何具体的地位取值,属于典型难懂易用的语法。

总结

学会 TS 根底语法后,活用才是要害。

探讨地址是:精读《type challenges – easy》· Issue #422 · dt-fe/weekly

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

关注 前端精读微信公众号

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

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

正文完
 0