TS类型体操 之 循环中的键值判断,as 关键字使用

7/3/2022 TS

# TS 类型体操 之 循环中的键值判断,as 关键字使用

这里将要讲几个题目,非常的有代表性,而且这些题目用到了很多的关键字和 TS 语法

3・实现 Omit 8・Readonly 2 2595・PickByType 2757・PartialByKeys

  • 如何在对象的循环中给键值做判断
  • P in keyof as any extends P 是什么意思
  • 如果一定要用到 & 符,如何在返回之前合并 & 2 边的内容

上面挑选的几题,其实解题套路都一样,了解了上面 3 个问题后,这几题套模版就能解决

# 00003-medium-omit 题目实现

需求: Omit 会创建一个省略 K 中字段的 T 对象。

这和 Pick 很像,只是结果相反,Pick 是挑选需要的字段,而 Omit 则是排除指定的字段

  • 根据现了解的关键字, keyof 肯定要用,可是怎么排除其他的键? 用 never 来做键,就能在这个字段值排除了

所以就引出了第一个问题 如何在对象的循环中给键值做判断

先看答案:

type MyOmit<T, K extends keyof T> = {
  [P in keyof T as P extends K ? never : P]: T[P]
}
1
2
3

K extends keyof T 这些语法就不解释了,和 Pick 一样,如果不了解的可以看看第一篇文章,TS 体操的前置知识

关键就在于 P extends K ? never : P 判断 P 这个键是否属于 K 的范围,属于的则是要排除的,返回 nerver,不属于的就需要返回当前的 P,代表把当前的键保留下来


看到这里又引出第二个问题 P in keyof as any extends P 是什么意思

加一些括号好理解一下

[
  (P in keyof T) as
  (P extends K ? never : P)
]
1
2
3
4

以 as 为分界,把这段代码分为 2 段

  • P in keyof T 是遍历的意思,这个好理解
  • as 在这里姑且认定为是 断言
  • P extends K ? never : P 这里是为了判断类型

连起来怎么看

  • P 在 T 的范围循环
  • P 得到的是 T 中的键
  • 对于这个 P 我们为他断言 为 P / never
  • 如果 P 的这个键在 K 的范围中,我们就断言当前的 P 是 never(抛弃原先 P 的值),那么在对象循环的时候 never 就会被忽略掉,从而实现 Omit

# 00008-medium-readonly-2 题目详解

需求:这个就是 readonly 的 plus 版,指定字段来 readonly,如果没指定的则当作全部 readonly

好家伙,刚学完 Pick 和 Omit,又是指定字段,这不是手到擒来吗,用上 & 合并一下就完事了

下面是错误示范:

当时的错误思路是这样的:既然是指定字段 readonly,那我拿 原对象 和 指定字段循环出来的对象合并一下,循环的对象加个 readonly,让后面的字段覆盖前面的,那不就达成效果了吗

// 错误示范
type MyReadonly21<T, K extends keyof T = keyof T> = T & {
  readonly [P in K]: T[P]
}
1
2
3
4

结果肯定是报错的,因为 & 运算符计算出来的是 交集 ,简单点用段代码来说就是

type testReadonly = { title: string; name: string } & { readonly title: string; name: string }

// test 会报错:提示缺少了title字段
// Property 'title' is missing in type '{ name: string; }' but required in type '{ title: string; name: string; }'.
const test: testReadonly = {
  name: '111'
}
1
2
3
4
5
6
7

按道理 title 字段合并后应该也是只读,可是他变成了必填的了。 交集 细品,把 readonly 理解为 title 字段的一个额外的标签


正确答案如下:

用 Omit 挑选出必填的字段,然后在用 Pick 挑选出 readonly 的对象,这 2 个对象合并才是正确的答案

type MyReadonly2<T, K extends keyof T = keyof T> = {
  [P in keyof Omit<T, K>]: T[P]
} & {
  readonly [P in K]: T[P]
}
1
2
3
4
5

也有复杂的方案,就是假装我不会用 Omit ,用上面刚学的套路也可以解决这个问题

type MyReadonly21<T, K extends keyof T = keyof T> = {
  [P in keyof T as P extends K ? never : P]: T[P]
} & {
  readonly [P in K]: T[P]
}
1
2
3
4
5

# 02595-medium-pickbytype 题目实现

这个题目就和上面的套路一模一样!!也是需要在循环的时候就决定好哪些 key 要保留

不同的是要根据不同字段对应的类型来进行筛选,也就是说之前的 [P in keyof T as P extends U], 要换成 [P in keyof K as T[P] extends U ? nerver : P]

注意 T[P] extends U 的写法!就这么一个参数的变化,其余的该拿 P 还是拿 P,该 never 还是 never,这题就解决了

# 02757-medium-partialbykeys 题目详解

需求:指定字段来设置选填,如果没指定的则当作全部都选填

正是这道题,引出的问题 3: 如果一定要用到 & 符,如何在返回之前合并 & 2 边的内容

看到这个问题,是不是和 readonly2 的需求一样,无非就是把 readonly 换成 ?

代码啪一下就写完了,然后看测试用例 一个都没过

type PartialByKeys<T, K extends keyof T = keyof T> = {
  [P in keyof T as P extends K ? never : P]: T[P]
} & {
  [P in K]?: T[P]
}
1
2
3
4
5

可是仔细观察字段,该必填的有必填,该选填的有?。除了案例给出的是一个完整的对象,而我的是一个 {} & {} 交叉运算出来的,其他没啥区别呀

然后我仔细研究了一下测试用例,注意看 02757-partialbykeys 这题的测试用例,用的是 Equal 工具类

type cases = [
  Expect<Equal<PartialByKeys<User, 'name'>, UserPartialName>>,
  Expect<Equal<PartialByKeys<User, 'name' | 'unknown'>, UserPartialName>>,
  Expect<Equal<PartialByKeys<User, 'name' | 'age'>, UserPartialNameAndAge>>,
  Expect<Equal<PartialByKeys<User>, Partial<User>>>
]
1
2
3
4
5
6

而 08-readonl2 的测试用例,用的是 Alike

type cases = [Expect<Alike<MyReadonly21<Todo1>, Readonly<Todo1>>>]
1

好家伙。。如此说来, readonl2 算起来还不是完全的标准答案,顶多打个 98 分?

要解决这个问题也非常容易,我们只需要把 2 个合并一下就好了,至于怎么合并?

又有一个小妙招 —— 用 Pick,因为 Pick 能把字段都提出来,然后合并成一个新的对象,我们只需要把我们全部字段都放进去提取一次,就能合并在一起了

type Clone<T> = Pick<T, keyof T>

type PartialByKeys<T, K extends keyof T = keyof T> = Clone<
  {
    [P in keyof T as P extends K ? never : P]: T[P]
  } & {
    [P in K]?: T[P]
  }
>
1
2
3
4
5
6
7
8
9

到这里,常规的示例已经通过了,还留下一个 PartialByKeys<User, 'name' | 'unknown'> 在报错

因为他这里第二个字段传入的不一定在 User 里面,比如 unknown 就不在 User 的 key 中,而我们 K extends keyof T = keyof T 这个限制了参数的进来,所以只能把限制去掉,改成下面这样

type PartialByKeys2<T, K = keyof T> = Clone<
  {
    [P in keyof T as P extends K ? never : P]: T[P]
  } & {
    [P in K]?: T[P]
  }
>
1
2
3
4
5
6
7

去掉限制后,又新增了 2 个新的报错

  • K 的报错 Type 'K' is not assignable to type 'string | number | symbol'.
  • T[P] 的报错 Type 'P' cannot be used to index type 'T'.

因为确实没限制 k 的类型,而且 in 循环的则是 unio(联合类型)的变量
T[P] 因为 K 不一定就是 T 里面的键,T['unknown'] 肯定也是不存在的,所以报错了

所以当 K 循环的时候,还是用回刚才学的套路,在循环中判断键值 [P in K as P extends keyof T ? P : never]

T[P] 的问题解决就是先判断一下 P 是在 T 的范围内的才读 T[P] 就 OK 了

完整的正确答案如下:

type PartialByKeys2<T, K = keyof T> = Clone<
  {
    [P in keyof T as P extends K ? never : P]: T[P]
  } & {
    [P in K as P extends keyof T ? P : never]?: P extends keyof T ? T[P] : never
  }
>
1
2
3
4
5
6
7

# 最后

复盘一下几个问题

  • 如何在对象的循环中给键值做判断
    • as 关键字,as 后面接上条件语句,如果为 false,则返回 never 就可以排除掉某个键值了
  • P in keyof as any extends P ? never : P 是什么意思
    • P in keyof 是一组
    • extends P ? never : P 是一组
    • 最后 2 组的值用 as 链接起来,形成当前循环的键
  • 如果一定要用到 & 符,如何在返回之前合并 & 2 边的内容
    • & 符号是一个交叉合并的过程,合并出来的对象在约束数据的功能上没有区别
    • 可是交叉运算出来后的数据和单一类型用严格相等对比得到的结果是 false
    • 最后可以基于 Pick 工具,封装一个 Clone 工具,把 {} & {} 合并为一个 {}
Last Updated: 1/7/2024, 5:51:59 PM