类型体操之实现 Pick 和 Omit

182 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第17天,点击查看活动详情

之前深信服前端一面的时候,面试官问我会 TypeScript 吗?想到我之前说 Vue3 原理时顺便提了一口 Vue3 是基于 TypeScript 写的,我既然懂 Vue3 原理,那说不会一点 TypeScript 那实在是有点说不过去

结果就是面试官直接让我实现一个 Pick,直接戳穿了一个只用过 interfacetype 和泛型的 ts 小白,我虽然背过很多八股文,但很明显没背过 ts 的类型实现,而且只用过一两次 Pick,所以今天就借这篇文章写一写 PickOmit 的实现

为什么还要写 Omit,因为面试官说我不会写 Pick 可以写一个 Omit,但我两个都不会 /(ㄒoㄒ)/~~

202212012137235.png

type-challenges

如果你已经了解过关于 type-challenges 和其对应测试方法,可以跳过本章节,直接阅读Pick 章节部分

想要去试试水 ts 类型体操可以去 type-challenges/type-challenges 这个库,里面有很多奇奇怪怪的类型,不常见的那种

本项目意在于让你更好的了解 TS 的类型系统,编写你自己的类型工具,或者只是单纯的享受挑战的乐趣!我们同时希望可以建立一个社区,在这里你可以提出你在实际环境中遇到的问题,或者帮助他人解答疑惑 - 这些问题也可能被选中成为题库的一部分!

那么问题来了,PickOmit 在不在里面呢?

,而且是第 3 道和第 4 道题目,悔恨没有面试前看过这个库

在线测试

当我们选择题目之后,会有一个在线测试的链接

IMG

202212152201034.png

测试的目的是让框住部分的异常提示消失

IMG

Playground 的运行是不会有任何结果的,我写这个挑战的时候曾经一度以为是运行得到是否正确,但我一直没有运行成功

由于它里面有提供 Expect 等类型,我仍然怀疑是可以运行得到结果的,留个悬念,等我弄清楚了再来更新

Pick

题目描述

实现 TS 内置的 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,
}

测试用例

/* _____________ 测试用例 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<Expected1, MyPick<Todo, 'title'>>>,
  Expect<Equal<Expected2, MyPick<Todo, 'title' | 'completed'>>>,
  // @ts-expect-error
  MyPick<Todo, 'title' | 'completed' | 'invalid'>,
]

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

interface Expected1 {
  title: string
}

interface Expected2 {
  title: string
  completed: boolean
}

题目模板

/* _____________ 你的代码 _____________ */

type MyPick<T, K> = any

解答

Pick 就是挑选的意思,描述里面就是挑了 titlecompleted 两个属性,那么在 ts 的层面需要如何实现呢?

首先你得保证 titlecompletedTodo 里面的属性,不是的话还挑什么呢?

对于这部分可以使用 keyof,其实 keyofJS 中也有个很像的 API,就是 Object.keys()Object.keys 的作用就是返回对象的 key(键) 列表,那么你应该知道 keyof 是干什么的了,就是用于获取某种类型的所有键,其返回类型是联合类型,它是 ts 的一个操作符

因此第一步实现如下,表示 KT 类型中的键

type MyPick<T, K extends keyof T> = any

回到描述里面的 TodoPreview,其实它的类型如下

interface TodoPreview {
  title: string
  completed: boolean
}

TodoPreview 其实就是 MyPick 返回的,换言之

type MyPick<Todo, 'title' | 'completed'> = {
  title: string
  completed: boolean
}

因此实现的思路就是需要在 MyPick 实现中补充一个泛型表示原 Todo 的键和对应类型,即如下

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

这里补充一下 ts 泛型的概念,ts 的泛型可以用来表示未确定的类型,同时在泛型之上添加约束,比如 K extends keyof T 即表示 T 所有键里面的任意一个

[P in K] 里面的 in 就不是这个意思了,inJS 的原生运算符,如果指定属性在指定对象中,则 in 运算符返回 true,而对于 ts 来说,这种语法 [P in K] 可以用来遍历联合类型里的类型(注意,遍历每一个),这也就是为什么不用 [P extends K],作用不一样哈

T[P] 的意思就比较好理解了,interface 具象化为对象,T[P] 不就是表示 T 中属性为 P 的类型值吗?即 Todo['title']: string

至此 Pick 实现,关键点为 keyofextendsin,泛型