typescript 类型体操 之 12-medium-chainable-options

1,120 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第12天,点击查看活动详情

前言

在学习typescript的过程当中,有一个github库对其类型的学习特别有帮助,是一个有点类似于leetcode的刷题项目,能够在里面刷各种关于typescript类型的题目,在上一篇文章中,我们完成了中等的第五题,今天来做中等的第六题 12-medium-chainable-options

下面这个是类型体操github仓库:

type-challenges/type-challenges: Collection of TypeScript type challenges with online judge (github.com)

12-medium-chainable-options

今天要实现的工具类型可能比较难理解,但是一定要先理解完题目的意思和要求,再去研究怎么解答或者查找题解,不然是完全看不明白的。

image.png

import type { Alike, Expect } from '@type-challenges/utils'

declare const a: Chainable

const result1 = a
  .option('foo', 123)
  .option('bar', { value: 'Hello World' })
  .option('name', 'type-challenges')
  .get()

const result2 = a
  .option('name', 'another name')
  // @ts-expect-error
  .option('name', 'last name')
  .get()

type cases = [
  Expect<Alike<typeof result1, Expected1>>,
  Expect<Alike<typeof result2, Expected2>>,
]

type Expected1 = {
  foo: number
  bar: {
    value: string
  }
  name: string
}

type Expected2 = {
  name: string
}

首先要先来理解一下什么是 可串联构造器,他就是一个可以连续调用的对象,并且最后在调用了get以后会返回一个对象类型。

所有的 option 函数都是在为最后的这个对象添加属性,而 get 方法就会返回最后的这个对象类型。

利用 JS 进行对比学习

这道题比较特殊,跟之前碰到过的工具类型都比较不同,能够通过不断地调用一个option方法来为一个对象添加属性,并且在调用get方法之后返回这个对象。

那么首先我们先定义一个对象a用来保存option方法和get方法,并且需要在a内部定义一个对象用来存储option定义出来的键值,还有一个option方法能够添加键值,为了能够递归调用,那么option方法在最后一定得返回a,再接着还需要一个get方法返回a中定义的对象。

let a = {
  Obj:{},
  option:function(key,val){
    if(typeof val != 'string'){
      this.Obj[key] = this.option(val)
      return a
    }else{
      this.Obj[key] = val
      return a
    }
  },
  get:function(){return this.Obj}
}

image.png

这就是 JS 版本大概的实现,能先通过 JS 来大概体会一下,题目要我们实现的是一个什么样的对象类型。

实现 Chainable

通过 JS 实现我们能大概看出来,这个对象中应该会有两个函数,第一个函数会一直给一个对象添加属性,另一个函数则是在调用的时候,返回这个对象,那么最开始的思路就有了,我们需要创建一个对象类型,这个对象类型内部有两个方法,第一个方法会不断地为一个对象添加属性,另一个的返回值就是这个对象,那么这个定义在 a 中的对象,我们能够通过 泛型来进行保存,并且付给它一个默认值也就是空对象 = {}

type Chainable<T extends object = {}> = {
  option():void
  get(): T
}

然后我们就需要在 option 中去为这个对象添加属性,这里可以使用 Record 工具类型,传入键值,就会为我们返回一个对象类型。

type A6 = Record<'bar', { value: 'Hello World' }>
// type A6 = {
//   bar: {
//       value: 'Hello World';
//   };
// }

并且在 JS 代码中我们能够得到,option 这个方法,需要在返回 a 这个类型,并且因为我们是把返回对象通过泛型参数传入的,所以可以通过交叉类型来把旧的对象和新的option生成的对象类型做一个合并在作为参数返回。

这样我们就能够得到最后的答案了:

type Chainable<T extends object = {}> = {
  option<K extends string, V>(key: K, value: V): Chainable<T & Record<K, V>>
  get(): T
}

但是这时候会发现,测试中又有一个经典的老朋友报错了

image.png

这说明,这行注释下面的代码应该要报错,不然注释就会报错,下面这行代码能够看出来,option 重复传入了两个同样的键,那么我们就需要对传入的 Key 做一个判断,看看是否存在原本的对象当中,存在的话就要赋值为 never 产品报错。

type Chainable<T extends object = {}> = {
  option<K extends string, V>(key: K extends keyof T ? never : K, value: V): Chainable<T & Record<K, V>>
  get(): T
}

这样到这里,测试用例就能够全部通过了。

知识点

关于上述提到了部分的知识点:

  1. Record 工具类型
  2. never 的使用
  3. 递归

never 代表永远不存在的值,能够赋值给一个变量这样在这个变量被赋值的时候就会产生报错,用于定义在代码中本应该不会运行到的部分

image.png

或者 never 还能够用来去除对象中的键,这点在映射类型的重映射中经常使用,可以看看之前的文章:

TypeScript 之 映射类型(重映射) - 掘金 (juejin.cn)

总结

今天我们做完了中等的第六题,题目的展现方式很不一样,但是实用性还是特别高的,特别是在定义一些我们一开始没办法保证的对象类型,这算是一种很不错的实践。