「TS类型体操00004」实现 Pick

352 阅读3分钟

题目

实现一个类型工具函数 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,
}

原题链接

实现Pick

分析测试用例

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

  1. 返回一个对象。
  2. 遍历 keys
  3. todo[key] 取值。
  4. 检查 key 是否在 todo 里。

翻译 JS 逻辑点为 TS

1. 返回一个对象

TS 中,这个需求写一个 {} 即可实现。

type MyPick<T,K> = {}

2. 遍历 keys

keysTS 里是 K, 它可能是 'title' | 'completed' 这种形式,这是一个联合类型(union)

如何遍历一个联合类型?答案很简单,一个 in 就行。

// 遍历 K, 每个遍历的值声明为 P。
// 比如 K 是 'title' | 'completed',那遍历时 P 第一次是 title, 第二次是 completed。
[P in K]

3. todo[key] 取值

TS 中的取值和 JS 是一样的,用 [] 取属性值即可。

这里取出 TP 属性值:

[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];
}