这里将要讲几个题目,非常的有代表性,而且这些题目用到了很多的关键字和 TS 语法
上面挑选的几题,其实解题套路都一样,了解了上面 3 个问题后,这几题套模版就能解决
需求: Omit 会创建一个省略 K 中字段的 T 对象。
这和 Pick
很像,只是结果相反,Pick 是挑选需要的字段,而 Omit 则是排除指定的字段
never
来做键,就能在这个字段值排除了所以就引出了第一个问题 如何在对象的循环中给键值做判断
先看答案:
type MyOmit<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P]
}
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)
]
以 as 为分界,把这段代码分为 2 段
P in keyof T
是遍历的意思,这个好理解P extends K ? never : P
这里是为了判断类型连起来怎么看
P / never
需求:这个就是 readonly 的 plus 版,指定字段来 readonly,如果没指定的则当作全部 readonly
好家伙,刚学完 Pick 和 Omit,又是指定字段,这不是手到擒来吗,用上 &
合并一下就完事了
下面是错误示范:
当时的错误思路是这样的:既然是指定字段 readonly,那我拿 原对象 和 指定字段循环出来的对象合并一下,循环的对象加个 readonly,让后面的字段覆盖前面的,那不就达成效果了吗
// 错误示范
type MyReadonly21<T, K extends keyof T = keyof T> = T & {
readonly [P in K]: T[P]
}
结果肯定是报错的,因为 &
运算符计算出来的是 交集 ,简单点用段代码来说就是
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'
}
按道理 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]
}
也有复杂的方案,就是假装我不会用 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]
}
这个题目就和上面的套路一模一样!!也是需要在循环的时候就决定好哪些 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,这题就解决了
需求:指定字段来设置选填,如果没指定的则当作全部都选填
正是这道题,引出的问题 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]
}
可是仔细观察字段,该必填的有必填,该选填的有?。除了案例给出的是一个完整的对象,而我的是一个 {} & {}
交叉运算出来的,其他没啥区别呀
然后我仔细研究了一下测试用例,注意看 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>>>
]
而 08-readonl2 的测试用例,用的是 Alike
:
type cases = [Expect<Alike<MyReadonly21<Todo1>, Readonly<Todo1>>>]
好家伙。。如此说来, 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]
}
>
到这里,常规的示例已经通过了,还留下一个 PartialByKeys
在报错
因为他这里第二个字段传入的不一定在 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]
}
>
去掉限制后,又新增了 2 个新的报错
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
}
>
复盘一下几个问题
as
关键字,as 后面接上条件语句,如果为 false,则返回 never 就可以排除掉某个键值了{} & {}
合并为一个 {}