Omit
问题描述
不使用 Omit 实现 TypeScript 的 Omit<T, K> 泛型。
Omit 会创建一个省略 K 中字段的 T 对象。
例如:
interface Todo {
title: string
description: string
completed: boolean
}
type TodoPreview = MyOmit<Todo, 'description' | 'title'>
const todo: TodoPreview = {
completed: false,
}
// ============= Test Cases =============
import type { Equal, Expect } from './test-utils'
type cases = [
Expect<Equal<Expected1, MyOmit<Todo, 'description'>>>,
Expect<Equal<Expected2, MyOmit<Todo, 'description' | 'completed'>>>
]
// @ts-expect-error
type error = MyOmit<Todo, 'description' | 'invalid'>
interface Todo {
title: string
description: string
completed: boolean
}
interface Expected1 {
title: string
completed: boolean
}
interface Expected2 {
title: string
}
// ============= Your Code Here =============
// 答案1
type MyExclude<T, K> = T extends K ? never : T
type MyOmit<T, K extends keyof T> = {
[P in MyExclude<keyof T, K>]: T[P]
}
// 错误答案1
// type MyOmit<T, K extends keyof T> = {
// [P in keyof T extends K ? never : keyof T]: T[P]
// }
// 答案2
// type MyOmit<T, K extends keyof T> = {
// [P in keyof T as P extends K ? never : P]: T[P]
// }
// 答案3
// type MyOmit<T extends object, K extends keyof T> = Pick<T,MyExclude<keyof T, K>>;
Omit 类型很好理解,首先,第一个泛型参数是一个对象类型,第二个泛型参数为第一个泛型参数的属性,这就要求我们在一开始约束参数的类型,这里最好使用 T extends object 来约束第一个泛型参数的类型,这里的测试用例中使用的都是 interface ,所以并没有错误,但是你不能保证使用时,传入的第一个泛型参数一定是对象类型。第二个参数一定是第一个泛型参数的属性,所以约束为 K extends keyof T,如果不约束,那么 MyOmit<Todo, 'description' | 'invalid'> 将不会报错。其次,我们最终得到的类型仍然是一个对象类型,所以结构一定是 type MyOmit<T, K extends keyof T> = {},在 {} 写接下来的逻辑,先来确定键名,键名为排除了第二个泛型参数的键名,如何排除呢,我们可以使用 Exclude类型来实现,该类型是指排除第一个泛型参数中第二个泛型参数,Exclude<keyof T, K> 就是指在泛型 T,的所有 key 中排除 K, type MyExclude<T, K> = T extends K ? never : T 和内置类型实现是一样的,这就得到排除了泛型 K ,得到了排除 K 的对象,P in MyExclude<keyof T, K> 就是循环对象中的键名,T[P] 得到键值。当然会有人想到,既然这样,我为什么不可以把 P in MyExclude<keyof T, K> 中的 MyExclude<keyof T, K> 直接展开写在原本的位置呢,即将 [P in MyExclude<keyof T, K>] 改为 [P in keyof T extends K ? never : keyof T] ,然而,这样会报错,因为你不能确定其中 keyof T 的值是不是 P ,所以测试用例不能通过,想要通过,则可以改为 [P in keyof T as P extends K ? never : P] 使用断言,确定 keyof T 的值。
答案3:Pick 内置类型,是指从第一个泛型参数中挑出第二个泛型参数,所以第一个泛型参数必须为键名组成的联合类型,第二个泛型参数必须为第一个泛型参数的值,这里有个小技巧,如果你不知道 keyof T 的具体结果是什么,比如:
interface Todo {
title: string
description: string
completed: boolean
}
type keyOfTodo=keyof Todo
// 这里鼠标放在 keyOfRTodo 上只会显示 keyof Todo,但是如果你这么写
type keyOfTodo=keyof Todo & {}
// 这里鼠标放在 keyOfRTodo 上就会显示具体的值
"description" | "completed" | "title"
也就是说 Pick 类型会从 T 中拿出排除 K 之后的键值对,也能得到正确结果。