题目
实现一个类型工具函数 MyPick<T,K>。
其中,K 是一个属性的集合 (类比于JS的数组), T 是一个interface(类比于JS的对象),请从类型 T 中筛选出包含在 K 中的属性,去掉不包含在 K 中的属性,从而构造出一个新的类型。
例如:
// 实现 MyPick
// type MyPick<T,K> = ...
// 定义T
interface Todo {
title: string
description: string
completed: boolean
}
// 通过你实现的 MyPick 筛选出属性
// 本例中筛选出 title 和 completed 属性, 去掉 description 属性
type TodoPreview = MyPick<Todo, 'title' | 'completed'>
// 在项目中使用筛选后的类型
const todo: TodoPreview = {
title: 'Clean room',
completed: false,
}
原题链接
分析测试用例
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
// 测试用例1: 筛选出 title
Expect<Equal<Expected1, MyPick<Todo, 'title'>>>,
// 测试用例2: 筛选出 title 和 completed
Expect<Equal<Expected2, MyPick<Todo, 'title' | 'completed'>>>,
// 测试用例3: 'invalid' 不在 Todo 的属性中,筛选时应该报错。
// @ts-expect-error (期望下一行报错)
MyPick<Todo, 'title' | 'completed' | 'invalid'>,
]
// 这个是 T
interface Todo {
title: string
description: string
completed: boolean
}
// K 为 'title' 时, 正确 MyPick 后达到的效果
interface Expected1 {
title: string
}
// K 为 'title' | 'completed' 是, 正确 MyPick 后达到的效果
interface Expected2 {
title: string
completed: boolean
}
思路
类比 JavaScript
编程语言的思维是相通的,我们对 JS 熟悉,所以可以先按 JS 的思维来解这道题,解决了以后再翻译成 TS。
// 待实现的类型工具
// type MyPick<T,K> = ...
// 开始类比
// 1. 把 `类型工具MyPick` 类比成一个函数
// 2. 把 `接口T` 类比成一个对象 todo
// 3. 把 `属性集合K` 类比成一个数组 keys
function MyPick(todo, keys) {
// 最后要返回一个被筛选属性后的新类型,类比到 JS 就是返回一个新对象。
// 创建新对象
const obj = {};
// 将 keys 所包含的 todo 的属性,筛选出来,赋值到新对象里。
keys.forEach(key => {
// 赋值之前,必须先检查这个属性是不是 todo 的属性之一
// 是 todo 的属性才能被筛选出来
// 不是则不能被筛选,也就是新对象不能得到它。
if (key in todo) {
obj[key] = todo[key];
}
});
// 返回新对象
return obj;
}
通过类比,总结用到的 JS 逻辑点,准备翻译成 TS。
- 返回一个对象。
- 遍历
keys。 todo[key]取值。- 检查
key是否在todo里。
翻译 JS 逻辑点为 TS
1. 返回一个对象
在 TS 中,这个需求写一个 {} 即可实现。
type MyPick<T,K> = {}
2. 遍历 keys
keys 在 TS 里是 K, 它可能是 'title' | 'completed' 这种形式,这是一个联合类型(union)。
如何遍历一个联合类型?答案很简单,一个 in 就行。
// 遍历 K, 每个遍历的值声明为 P。
// 比如 K 是 'title' | 'completed',那遍历时 P 第一次是 title, 第二次是 completed。
[P in K]
3. todo[key] 取值
TS 中的取值和 JS 是一样的,用 [] 取属性值即可。
这里取出 T 的 P 属性值:
[P in K]: T[P]
// 综合起来
type MyPick<T,K> = {
[P in K]: T[P]
}
4. 检查 key 是否在 todo 里
这个需求在 TS 中应该解释为:
对于类型工具 MyPick<T,K>,如果 K 有属性不在 T 中,编译时就得报错。
那么如何告诉编译器 K 的所有属性一定要存在于 T 中,不存在就得报错呢?
我们可以用 extends 操作符,把 K 限定在 T 的属性集合的范围内。
然而 T 是一个 interface,为了取到 T 的属性集合,我们要用 keyof。
所以这个需求就要这么解决:
type MyPick<T, K extends keyof T> = ...
实现
综上所述,类型工具 MyPick 实现如下:
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
}